Measure code placement instead of arguing about it (#240) - #249
Conversation
This crate has been bitten by placement four times. A rustc minor version moved the parse path 15-96% (#120). A package-version bump did the same for a binary whose x86-64 instruction stream was byte-identical -- 88 instructions, only label hashes differed (#204). And today three consecutive PRs failed the gate on parse_qp_message by +9.2%, +7.3% and +19.0%, for changes that cannot reach quoted-printable decoding at all: one of them touched only the parse_many thread scheduler, and on every one of those runs the same message in metadata mode -- same headers, decode skipped -- got faster. Two #[inline(never)] attempts moved the number to +7.5% and then to +12.9%. The gate's only remedy for that has been prose: re-run, and hope the next runner draw is a different CPU. A maintainer on an Apple M4 cannot reproduce the effect at all -- the same two builds that read 96% apart on Zen read +/-0.2% there -- and QEMU has no micro-op-cache model to reproduce it with. The hardware was never what was missing. The controlled experiment was. layout-ab.yml builds the same source K times differing only in -C metadata=layout<k>, which rustc hashes into the crate's StableCrateId -- the same thing a version bump changes, and so the same thing that moves symbol names, link order and the addresses of the hot loops. Every side is measured interleaved on one runner, so thermal drift lands on all of them alike. The spread across salts is that revision's layout sensitivity on that CPU, and it is the figure the gate's 7% threshold should be read against: a benchmark whose spread is 9% cannot produce a meaningful 9% verdict. Guards, because a measurement that quietly measures nothing is the failure mode here: the .so sha256 of every side must be pairwise distinct, or a salt did nothing and the run would report a reassuring 0%; and layout_spread.py refuses reports with no control benchmark, because without the pure-Python floor every spread below is uninterpretable. The classification rule is deliberately conservative. A benchmark is called layout-sensitive only if its spread clears both the control floor and 3%. Three, because #205 measured the version-bump effect at 3.2-4.4% on the gate's own CPU; below that a four-salt sweep cannot separate placement from the residual it sits in, and pretending otherwise would give this instrument a resolution it has not got. tests/test_layout_spread.py pins that rule case by case, including the two ways it can wrongly say "sensitive". Passing -f rustflags='-C llvm-args=-align-all-nofallthru-blocks=6' measures an alignment candidate as a second group, with a cost table, so the question "can the tail be removed from shipped x86_64 wheels" gets numbers rather than an opinion. Dispatch-only. Nothing under src/ or vendor/ changes, no build flag changes, and the shipped wheels are byte-identical to before. Signed-off-by: kurok <22548029+kurok@users.noreply.github.com>
|
Field evidence for why this workflow is needed, gathered since opening it. I rebased #246 and #247 onto
So on one revision, one CPU, unchanged source: placement alone moves the parse benchmarks ±15%, and which benchmark trips the 7% threshold is drawn fresh each build. That is the spread this workflow measures directly rather than by inference, and it says the gate is currently trying to resolve 7% inside a band twice that wide. Two plain dispatches would turn the estimate above into a number, per benchmark, with a control floor under it — and would tell you whether the threshold, the benchmark set, or the build flags are what needs to change. Nothing here changes any of that yet; this PR only adds the ability to find out. |
Part of #240 — deliverables 1 and 2. Deliverable 3 (the alignment-flag decision) needs dispatches that cannot run until this merges; see What is not here.
Branched from
master(c403a69).Why this went from housekeeping to the critical path today
#240 was filed as
priority: medium. It stopped being that this morning.Three consecutive PRs failed the benchmark gate on
test__fast_mail_parser___parse_qp_message:parse_qp_messageon x86headersdict cacheqp_message_metadata+7.3%parse_manythread scheduler#247 is the control that settles what this is. It touches
src/fast_mail_parser.rsandsrc/mail_parser.rsonly — nothing within reach of quoted-printable decoding. Its own benchmark works perfectly (parse_many_small_page−55.6%),qp_message_metadatais −0.3% flat, andparse_qp_messagereads +19.0% with per-round minima of 0.279 / 0.280 / 0.278. A thread-pool change cannot slow a decoder it never calls, and that is not noise.On #248 I spent three attempts patching it with
#[inline(never)]— first on the code I had changed, then onqp::decode_robustitself. Against an unmoving 0.246 ms base the three binaries read 0.268, 0.265, 0.277. It does not converge. That is what this PR is a response to: the repo has no instrument for the thing that is failing its gate, so every response to it is a guess.What this adds
.github/workflows/layout-ab.yml(dispatch-only) builds the same checkoutsaltstimes, each withRUSTFLAGS="-C metadata=layout<k>"and its ownCARGO_TARGET_DIR, then measures every side interleaved in one job.-C metadatais hashed into the crate'sStableCrateId— the same thing a version bump perturbs (#204) — so the sides do identical work and differ only in where it landed. Unlike the version-stringsedit keeps the wheel version and filename identical and needs nogit checkout Cargo.tomlafterwards..github/scripts/layout_spread.pyreports, per group and benchmark, the per-salt medians, the spread, the control floor, and a sensitive/not classification; with two groups it adds acost(b)table for the alignment candidate. It importsread_mins,read_machineandis_controlfromab_median.pyrather than copying them.Two guards, because an instrument that quietly measures nothing is the failure mode here:
.sosha256 must be pairwise distinct — a salt that produced identical bytes perturbed nothing, and the run would report a reassuring 0% that means only that;The classification rule is deliberately conservative: layout-sensitive iff the spread clears both the control floor and 3%. Three, because #205 measured the version-bump effect at 3.2–4.4% on the gate's own CPU — below that a four-salt sweep cannot separate placement from the residual it sits in, and claiming otherwise would give this a resolution it does not have.
Tests
tests/test_layout_spread.pypins cases (a)–(g) from the issue plus two more — 9 tests, all passing:::error::test__threaded___*is never classified sensitive, matching the gateCases (c) and (d) are the ones that matter: they are the two ways this could wrongly tell someone their code is fine when the runner was just noisy, or wrongly send them hunting a layout ghost.
What is not here, and why
The dispatches.
workflow_dispatchonly works for workflows present on the default branch, solayout-ab.ymlcannot be run until this merges. The acceptance criteria ask for two plain dispatches plus two per candidate flag with their summaries pasted here; I cannot produce those yet, and I would rather say so than imply the instrument has been exercised on real hardware. It has not — only its analysis path is tested, against synthetic reports.The alignment-flag decision (deliverable 3) is conditional on those numbers, so it is not here either.
.cargo/config.tomlis untouched and the shipped wheels are byte-identical to master.CONTRIBUTING.mdno longer says "nobody has measured how sensitive they are" — but it does not yet claim a measured spread either. It records today's three failures as the evidence that the effect is live, and points at the dispatch command. When the dispatches run, that paragraph should be rewritten again with the real per-benchmark numbers, andab_median.py's significant-verdict prose should quote the measured bound.Suggested order
Merge this, then dispatch twice plain. If the spread on
parse_qp_messageis large — which today's three failures suggest — that number explains #246 and #247 outright, and they can be judged against it rather than against a 7% threshold that turns out to be narrower than the instrument's own scatter.Checks run locally
pytest tests --ignore=tests/benchmark(820 passed, 3 skipped),ruff check .clean, the workflow YAML parses and its inputs are as documented. Nosrc/,vendor/,fast_mail_parser/,__init__.pyiortests/test_contract.pychange; aarch64/apple build flags untouched.