From 381303c7df55ff7bf2e8b5dc3e8054b968001aec Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 15:21:26 -0400 Subject: [PATCH 001/139] feat(platform-probes): add probe-queue-contention, which prices the tail claim Peeled from `mikegrier/deferred-namespace-ops`, where it was written alongside work that is not ready. It is landed on its own because it is an INSTRUMENT: it is useful before the plan that consumes it exists, and landing it first is what lets that decision be made against a measurement rather than an argument. The probe answers two questions a queue-shape decision is waiting on: whether the bounded array queue's tail claim contends badly enough to justify the linked and sharded MPSC shapes, and what `reserving_mpsc`'s extra read of the consumer's position actually costs. Two regimes, and the pair is the point. **Isolated** gives producers capacity large enough that nothing is refused and runs no consumer, so whatever curve appears against N is the claim and nothing else. **Drained** runs a consumer popping continuously, which is the only regime that can price the read of `head` -- that read is cheap until a consumer is WRITING the line, so measuring it in isolation would report it as free. Each row carries the refusal count from the queue's own `Observable` counters, so a consumer-bound plateau is visible as a fact rather than mistaken for contention. **It is deliberately absent from the CI probe job**, unlike every other probe, and the reason is a measurement rather than a preference: that job runs `cargo run` without `--release`, and in a debug build `mpsc` and `reserving_mpsc` come out at 249.7 and 254.0 ns/push at sixteen producers -- indistinguishable. In release, same machine, same minute: 193.5 and 52.2. A debug run does not merely lose precision, it reports the two shapes as equivalent, which is a confident wrong answer. It also wants more cores than a hosted runner has, and costs about a minute against a job whose other probes are seconds. Verified by running it rather than by building it: exit 0 in 64.8s on x86_64 16p/8c, 181 lines, banner and both regimes present. At sixteen producers, isolated: `baseline_fetch_add` 14.2 ns/push, `reserving_mpsc` 35.0, `permit_mpsc` 22.2, `slotwise_mpsc` 198.5. The `experimental-permit-claim` feature is enabled on the dependency because this probe is what decides its fate -- it has to be measured against the shipping shapes on the same host, in the same run, by the same harness. `dwcas` is what lets it instantiate the 128-bit claim layout. Two design-note corrections were needed on the way in, because the notes had been written against a state that has since moved: - The claim-word section said the three apportionments were "built as duplicates in claim_layout.rs so the shipping crate was not disturbed". That scaffolding is gone -- the layouts SHIP now, as `ClaimLayout` with `Balanced`, `Enduring`, `Perpetual` and `Wide`, which is what this probe imports. A reader who went looking for `claim_layout.rs` would not have found it. The duplicate-then-decide cycle closed and the note never said so. - Both sections linked to checklists that do not exist in this repository (`CHECKLIST-io-domains.md`, `CHECKLIST-claim-word-layout.md`); they arrive with the rest of the queue work. The notes now name the QUESTIONS rather than linking to items that would dangle, and say plainly that the plans carrying them land later. Every link in the added notes was checked to resolve before writing it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 1 + crates/windows-platform-probes/Cargo.toml | 17 + .../windows-platform-probes/DESIGN-NOTES.md | 192 ++++++ .../src/bin/queue_contention.rs | 326 +++++++++ crates/windows-platform-probes/src/lib.rs | 1 + .../src/queue_contention.rs | 641 ++++++++++++++++++ 6 files changed, 1178 insertions(+) create mode 100644 crates/windows-platform-probes/src/bin/queue_contention.rs create mode 100644 crates/windows-platform-probes/src/queue_contention.rs diff --git a/Cargo.lock b/Cargo.lock index 539b402e0..76ddfb335 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,6 +224,7 @@ dependencies = [ "windows-sys", "windows-threadpool-sys", "windows-topology-sys", + "windows-waitable-queues", "wtf-string", ] diff --git a/crates/windows-platform-probes/Cargo.toml b/crates/windows-platform-probes/Cargo.toml index db464bcba..66520872d 100644 --- a/crates/windows-platform-probes/Cargo.toml +++ b/crates/windows-platform-probes/Cargo.toml @@ -82,6 +82,10 @@ path = "src/bin/doorbell_cost.rs" name = "probe-request-cost" path = "src/bin/request_cost.rs" +[[bin]] +name = "probe-queue-contention" +path = "src/bin/queue_contention.rs" + # These two are the same code, and that is the measurement: they differ only in # whether `build.rs` embeds the `longPathAware` manifest, which is not a runtime # switch and so cannot be a flag on one binary. @@ -140,6 +144,19 @@ windows-placement-probe = { path = "../windows-placement-probe" } # a queue, not a stand-in, for the same reason the topology probe reads the # shipping parse: a reimplementation would measure the reimplementation. windows-namespace-request-sys = { path = "../windows-namespace-request-sys" } +# The contention probe measures the shipping queue shapes rather than a +# reimplementation, for the same reason: a stand-in would only measure itself, +# and the whole question is what the real tail claim costs. +# +# The experimental permit claim is enabled here because this probe is what +# decides its fate (SH-15.5): it must be measured against the shipping shapes on +# the same host, in the same run, by the same harness. +windows-waitable-queues = { path = "../windows-waitable-queues", features = [ + "experimental-permit-claim", + # So the probe can instantiate the 128-bit layout. The 64-bit ones need no + # feature; this is the only one that costs the queue crate a dependency. + "dwcas", +] } # The long-path probe measures a length against `MAX_PATH`, and `MAX_PATH` counts # UTF-16 code units. `OsStr::len` counts Rust's platform encoding -- WTF-8 here -- # so the two disagree the moment a non-ASCII character appears in `%TEMP%`, which diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 5417ffd1f..963e63f53 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -481,6 +481,198 @@ architecture. It would fail on any host with a longer user name, on either architecture. Recorded here because it is exactly the kind of result this comparison exists to classify correctly: a red build that is **not** a finding. +## The queue-contention probe, and why it must not run in the CI probe job + +`probe-queue-contention` measures two things a design decision is waiting on: whether the bounded +array queue's tail claim contends badly enough to justify the linked and sharded MPSC shapes, and +what [`reserving_mpsc`](../windows-waitable-queues/src/reserving_mpsc.rs)'s extra read of the +consumer's position actually costs. + +**The checklists carrying those decisions are not in this repository yet** -- they arrive with the +rest of the queue work -- so this note deliberately names the QUESTIONS rather than linking to items +that would dangle. The probe is the instrument; it is useful before the plan that consumes it lands, +and it is landed first precisely so the decision is made against measurement rather than argument. + +**It is deliberately absent from the `platform-probes` CI job, unlike every other probe, and the reason is +a measurement rather than a preference.** That job runs `cargo run` without `--release`. Measured in a +debug build, `mpsc` and `reserving_mpsc` come out at 249.7 and 254.0 ns/push at sixteen producers -- +indistinguishable. In release, on the same machine in the same minute, they are 193.5 and 52.2. The +un-inlined overhead of a debug build swamps the cache-coherence effects that *are* the finding, so a debug +run of this probe does not merely lose precision: it reports the two shapes as equivalent, which is a +confident wrong answer of exactly the kind this crate's `doorbell_cost` notes warn about. + +Two further reasons it stays out. A contention curve needs more cores than a hosted runner has, and the +32-producer rows on a four-core runner would measure the scheduler. And the run costs about two minutes in +release, against a job whose other probes are seconds. + +So this one is run by hand, on a known machine, and its numbers are recorded with the machine attached. + +### Reading it + +Two regimes, and the pair is the point. + +**Isolated** gives producers a capacity large enough that nothing is ever refused and runs no consumer, so +whatever curve appears against N is the claim and nothing else. **Drained** runs a consumer popping +continuously, which is the only regime that can price `reserving_mpsc`'s read of `head` -- that read is +cheap until a consumer is *writing* the line, and measuring it in isolation would report it as free. + +The drained regime has a **single** consumer, because that is what MPSC means, so at high producer counts +it becomes consumer-bound and a plateau there says nothing about the claim. Each row carries the refusal +count from the queue's own `Observable` counters precisely so that is visible as a fact rather than +mistaken for contention: the sixteen- and thirty-two-producer drained rows show millions of refusals and +should be read as measurements of the consumer. + +## The claim word's width costs 2-3x in isolation and much less in use + +Measured by `probe-queue-contention` on one host, `x86_64-pc-windows-msvc`. +Three apportionments of `reserving_mpsc`'s claim word: 32/32 and 16/48 over +`AtomicU64`, and 64/64 over `AtomicU128`. + +**These were duplicated scaffolding when the measurement was taken, and they +ship now.** The layouts were built as copies so the shipping crate was not +disturbed while the question was open; the measurement below is what closed it, +and they are now +[`ClaimLayout`](../windows-waitable-queues/src/reserving_mpsc.rs) with +`Balanced`, `Enduring`, `Perpetual` and `Wide` as its implementations -- which is +what this probe imports. Recorded because the original wording still described +the scaffolding, and a reader who went looking for `claim_layout.rs` would not +find it. + +`AtomicU128::is_always_lock_free()` is **true** on this target and +`cfg(target_feature = "cmpxchg16b")` is enabled by default, so the 128-bit +exchange is a compile-time-guaranteed native instruction here and no CPUID +branch was measured as though it were the algorithm. + +| producers | 16/48 vs 32/32 (isolated) | 64/64 vs 32/32 (isolated) | 64/64 vs 32/32 (drained) | +|---|---|---|---| +| 1 | 1.14x | 2.05x | 1.05x | +| 4 | 1.21x | 1.37x | 1.12x | +| 8 | 1.00x | 2.33x | 1.07x | +| 16 | 0.88x | 2.37x | 1.00x | +| 32 | 0.98x | 2.99x | 1.11x | + +**Re-apportioning the bits is free.** 16/48 tracks 32/32 within noise in both +regimes, which is the expected result and worth stating as a confirmed +prediction rather than a discovery: both issue the same `lock cmpxchg` on the +same `u64`, so only the shift and mask constants differ. The 48-bit position +does force `head` and the per-slot `sequence` to 64 bits, and that cost does not +show up either. What this buys is the recurrence moving from 2^32 to 2^48 -- +from about 37 seconds of sustained maximum-rate pushing to about 28 days. + +**Widening the word is not free, and how much it costs depends entirely on the +regime.** Isolated, where the claim is the only thing happening, `cmpxchg16b` +costs 2-3x and the penalty *grows* with contention. Drained, with a consumer +running, it is 5-12%. + +### The drained regime flatters the slower layout, and the refusal counts say so + +The two regimes must not be averaged, and the drained one must not be read as +the answer on its own. **A slower producer is less backpressured**, so it earns +fewer refusals, and refusal retries are inside the timed region. At eight +producers the 64/64 layout took 12,149 refusals against 32/32's 74,181 -- so +part of what makes its per-push number look close is that it spent less time +being turned away. The drained figures are therefore an *understatement* of the +128-bit word's cost, not a measurement of it under load. + +The isolated regime is the clean measurement of the claim itself; the drained +one shows that in a queue doing real work the claim is not the dominant cost. A +real application sits between them, nearer the drained end the more +consumer-bound it is. + +### What the control caught + +The first run reported 3.7x against the shipping shape and a completely +different scaling curve. The cause was that the duplicate had not padded `head` +and the claim word onto separate cache lines, which `reserving_mpsc` does +deliberately -- every producer reads `head` on every push, so sharing a line +puts the consumer's writes in their path. Aligned, the duplicate tracks the +shipping shape's curve. + +A residual gap remains: the duplicate runs about 1.26x slower than +`reserving_mpsc` at high producer counts. That offset applies equally to all +three layouts, so the ratios above stand, but it means these figures are **not** +absolute numbers for the shipping shape and must not be quoted as such. + +**Comparing a duplicate against the original it stands in for is what made both +of these visible.** A run of three layouts that agreed with each other and +disagreed with reality would have looked entirely healthy. + +### What each apportionment actually buys + +The rollover figures for candidate splits, computed from the rates above. The +rate model reproduces the crate's own published figure -- 32/32 at 116M/s gives +37 seconds, which is what `reserving_mpsc`'s module documentation discloses -- so +these are an extension of that disclosure rather than a competing estimate. + +| split (reserved/position) | max outstanding reservations | @257M/s | @116M/s | @33M/s | +|---|---|---|---|---| +| 32/32 (ships) | 2^32 | 17 s | 37 s | 2.2 min | +| 24/40 | 2^24 | 71 min | 2.6 hr | 9.2 hr | +| 21/43 | 2^21 | 9.5 hr | 21.1 hr | 3.1 days | +| 20/44 | 2^20 | 19.0 hr | 42.1 hr | 6.1 days | +| 16/48 | 2^16 | 12.7 days | 28.1 days | 98 days | +| 12/52 | 2^12 | 202 days | 449 days | 4 yr | +| 8/56 | 2^8 | 9 yr | 20 yr | 69 yr | +| 64/64 (`u128`) | 2^64 | 2,270 yr | 5,039 yr | 17,607 yr | + +Rates: 257M/s is the measured isolated peak at one producer, which has no +consumer and so is not a rate any draining queue can sustain -- it is a +conservative floor on time-to-wrap. 33M/s is the measured drained rate at one +producer. 116M/s is the crate's own disclosed figure and is the honest planning +number. + +**The reservation half is where the bits are being spent, and it is the half +worth least.** Outstanding reservations are bounded by how many producers are +mid-flight -- hundreds, perhaps thousands -- and the field currently holds four +billion. Giving up reservations nobody will allocate is what buys the position +bits: 2^21 reservations leaves about a day, 2^12 leaves over a year, and 2^8 +leaves twenty years. The last is the same practical answer a 128-bit word gives, +on a plain `AtomicU64`, at no measured cost, without a third-party dependency and +without reopening `D-18`'s i686 question. + +So the candidates worth considering are **12/52 and 8/56**, not the 16/48 first +sketched here: 16/48's 12.7 days at the conservative floor is still reachable by +a busy long-lived process, and 12/52 is the first row that is not. + +### Re-measured on the shipping type, and the duplicate had understated the wide word + +`CW-1.6` deleted the duplicated protocol in this crate once +`windows-waitable-queues` took the layout as a parameter, so the probe now +instantiates the real type at each layout. The numbers below supersede the ones +above, which were taken from the stand-in. + +| producers | 16/48 vs 32/32 | 8/56 vs 32/32 | 64/64 vs 32/32 | +|---|---|---|---| +| 1 | 1.02x | 0.98x | 1.45x | +| 4 | 1.04x | 1.01x | 1.33x | +| 8 | 1.05x | 1.05x | 1.59x | +| 16 | 1.21x | 1.21x | **3.83x** | +| 32 | 1.05x | 1.13x | **3.99x** | + +**The finding about apportionment survives contact with the real type.** Both +`u64` re-apportionments track the default within noise, including `Perpetual`'s +8/56 -- so buying twenty years of headroom really is free, and it is now +measured on the code that ships rather than on something resembling it. + +**The finding about width did not survive unchanged.** The duplicate reported +the 128-bit exchange at 2.37x and 2.99x at sixteen and thirty-two producers; the +real type reports 3.83x and 3.99x. The stand-in was *understating* the cost of +the layout it was built to evaluate, and by the widest margin exactly where the +decision is most sensitive. The conclusion is unaltered in direction and firmer +in degree. + +**The residual offset is gone, which is the point of the deletion.** The +duplicate ran about 1.26x slower than `reserving_mpsc` at high producer counts, +an error that had to be carried as a caveat on every figure. Running the same +configuration twice through the shipping type now agrees within noise -- 50.3 ns +against 52.1 ns at thirty-two producers -- because both rows are the same code. + +The general lesson is worth keeping even though the duplicate is gone: +**a stand-in is only evidence about the thing it stands in for while something +checks that it still does.** This one was checked, which is how the missing +cache padding was caught; but the checking only ever bounded the error, and the +bound was loose enough to hide a third of the wide word's cost. + ## The report is buffered, and what that costs diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs new file mode 100644 index 000000000..ce926ad49 --- /dev/null +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -0,0 +1,326 @@ +// Copyright (c) Mike Grier. + +//! Prints how the array queue's tail claim behaves as producers are added. +//! +//! **An experiment, not a component.** These probes measure platform behaviour +//! and are not for production use. Do not call them from production code, and +//! do not lift a technique out of here. See this crate's DESIGN-NOTES.md. +//! +//! This decides two things that are otherwise decided by taste: whether the +//! linked and sharded MPSC shapes are ever needed, and whether `mpsc` and +//! `reserving_mpsc` should merge. See `queue_contention`'s module docs. + +use windows_platform_probes::queue_contention::{PRODUCER_COUNTS, Run, measure, shapes}; +use windows_platform_probes::report::emit_report; + +fn main() { + // The probe's whole output policy, and it is one line: hand the renderer to + // the sink. Nothing here or below names a stream -- that is chosen once, in + // `report`, so retargeting a probe is not a rewrite. + emit_report(render); +} + +/// The probe's whole report, as text. +fn render(out: &mut dyn std::fmt::Write) { + // First line of the report, and part of the returned text rather than + // written out here: a captured report must carry the line naming the + // machine that produced it, and the taint marker with it. + let _ = writeln!( + out, + "{}", + windows_placement_probe::fingerprint::banner_line() + ); + let _ = writeln!(out, "== does the array queue's tail claim contend? ==\n"); + + let observation = measure(); + let _ = writeln!( + out, + "host reports {} logical processors\n", + observation.logical_processors + ); + + let _ = writeln!( + out, + "-- isolated: producers only, capacity large enough that nothing is refused --" + ); + render_table(out, &observation.isolated); + + let _ = writeln!( + out, + "\n-- drained: a consumer popping continuously, capacity 1024 --" + ); + render_table(out, &observation.drained); + + let _ = writeln!(out, "\ninterpretation:\n"); + + // Question 1: does the claim collapse as producers are added? + let _ = writeln!(out, " 1. tail-claim contention (isolated regime)\n"); + let _ = writeln!( + out, + " {:<18} {:>12} {:>12} {:>12} {:>14}", + "producers", "slotwise x1thr", "reserving", "permit", "atomic floor" + ); + for &producers in PRODUCER_COUNTS { + let mpsc = observation.scaling(&observation.isolated, shapes::SLOTWISE_MPSC, producers); + let reserving = + observation.scaling(&observation.isolated, shapes::RESERVING_MPSC, producers); + let permit = observation.scaling(&observation.isolated, shapes::PERMIT_MPSC, producers); + let floor = + observation.scaling(&observation.isolated, shapes::BASELINE_FETCH_ADD, producers); + let _ = writeln!( + out, + " {producers:<18} {:>12} {:>12} {:>12} {:>14}", + format_scaling(mpsc), + format_scaling(reserving), + format_scaling(permit), + format_scaling(floor) + ); + } + let _ = writeln!( + out, + "\n Read as: throughput at N producers divided by throughput at one." + ); + let _ = writeln!( + out, + " 1.00 means N threads together push no faster than one did." + ); + let _ = writeln!( + out, + " The atomic floor is the cheapest possible contended operation," + ); + let _ = writeln!( + out, + " so it says how much of any curve is the queue and how much is" + ); + let _ = writeln!( + out, + " simply what this processor does to a fought-over cache line." + ); + + // Question 2: what does reserving_mpsc's read of `head` actually cost? + let _ = writeln!( + out, + "\n 2. the price of reservation (drained regime, where `head` is written)\n" + ); + let _ = writeln!( + out, + " {:<18} {:>14} {:>14} {:>10} {:>14} {:>16}", + "producers", "slotwise ns/pu", "reserving", "ratio", "permit", "permit/reserving" + ); + for &producers in PRODUCER_COUNTS { + let plain = observation.find(&observation.drained, shapes::SLOTWISE_MPSC, producers); + let reserving = observation.find(&observation.drained, shapes::RESERVING_MPSC, producers); + let permit = observation.find(&observation.drained, shapes::PERMIT_MPSC, producers); + let ratio = format_ratio(reserving, plain); + // The column SH-15.5 exists to fill: the experimental claim against the + // shipping shape it would replace. Below 1.00 means the permit claim is + // cheaper; above means removing the room-decision race costs throughput. + let permit_ratio = format_ratio(permit, reserving); + let _ = writeln!( + out, + " {producers:<18} {:>14} {:>14} {:>10} {:>14} {:>16}", + format_nanos(plain), + format_nanos(reserving), + ratio, + format_nanos(permit), + permit_ratio + ); + } + let _ = writeln!( + out, + "\n `reserving_mpsc` reads the consumer's position on every push and" + ); + let _ = writeln!( + out, + " `mpsc` does not, which is the entire reason they ship as two" + ); + let _ = writeln!( + out, + " shapes. This regime is the one that can price that read, because" + ); + let _ = writeln!(out, " a consumer is writing the line being read."); + let _ = writeln!( + out, + "\n `permit_mpsc` is experimental and is the candidate replacement" + ); + let _ = writeln!( + out, + " for `reserving_mpsc`: it removes that read entirely, and with it" + ); + let _ = writeln!( + out, + " the stale room decision behind SH-14.1, by making admission a" + ); + let _ = writeln!( + out, + " read-modify-write on a permit count instead. The last column is" + ); + let _ = writeln!( + out, + " the trade -- below 1.00 and the safer claim is also the cheaper" + ); + let _ = writeln!( + out, + " one; above 1.00 and closing the hole costs throughput." + ); + + // Question 3: what does the claim word's apportionment and width cost? + let _ = writeln!(out, "\n 3. claim-word layout\n"); + let _ = writeln!( + out, + " Four apportionments of reserving_mpsc's claim word, measured on" + ); + let _ = writeln!( + out, + " the shipping type itself rather than on a stand-in. 32/32 is the" + ); + let _ = writeln!( + out, + " default; 16/48 and 8/56 are the same u64 exchange with the bits" + ); + let _ = writeln!( + out, + " apportioned differently; 64/64 is a u128 exchange (cmpxchg16b)." + ); + let _ = writeln!( + out, + " The three u64 rows issue the SAME instruction, so a difference" + ); + let _ = writeln!( + out, + " between them is noise or slot-metadata density, not the claim." + ); + let _ = writeln!( + out, + " 64/64 vs 32/32 prices the double-width exchange -- what removing" + ); + let _ = writeln!( + out, + " the recurrence outright costs, against 8/56 merely deferring it.\n" + ); + for (label, regime) in [ + ("isolated", &observation.isolated), + ("drained", &observation.drained), + ] { + let _ = writeln!(out, " -- {label} --"); + let _ = writeln!( + out, + " {:<10} {:>11} {:>11} {:>11} {:>11} {:>10} {:>10} {:>10}", + "producers", + "32/32 ns", + "16/48 ns", + "8/56 ns", + "64/64 ns", + "16/48 vs", + "8/56 vs", + "64/64 vs" + ); + for &producers in PRODUCER_COUNTS { + let narrow = observation.find(regime, shapes::CLAIM_NARROW, producers); + let deep = observation.find(regime, shapes::CLAIM_DEEP, producers); + let perpetual = observation.find(regime, shapes::CLAIM_PERPETUAL, producers); + let wide = observation.find(regime, shapes::CLAIM_WIDE, producers); + let _ = writeln!( + out, + " {:<10} {:>11} {:>11} {:>11} {:>11} {:>10} {:>10} {:>10}", + producers, + format_nanos(narrow), + format_nanos(deep), + format_nanos(perpetual), + format_nanos(wide), + format_ratio(deep, narrow), + format_ratio(perpetual, narrow), + format_ratio(wide, narrow) + ); + } + let _ = writeln!(out); + } + let _ = writeln!( + out, + " the 32/32 row and the reserving_mpsc row above are the same" + ); + let _ = writeln!( + out, + " configuration run twice, so they should agree within noise. They" + ); + let _ = writeln!( + out, + " are no longer a control against a duplicated implementation: the" + ); + let _ = writeln!( + out, + " shipping type takes the layout as a parameter, so there is nothing" + ); + let _ = writeln!( + out, + " left that could drift away from what callers actually run." + ); + let _ = writeln!( + out, + "\n CAUTION: the drained regime has ONE consumer, because that is what" + ); + let _ = writeln!( + out, + " MPSC means. At high producer counts it is expected to become" + ); + let _ = writeln!( + out, + " consumer-bound, and a plateau there says nothing about the claim." + ); + let _ = writeln!( + out, + " The refusal counts above are what make that visible: a run with" + ); + let _ = writeln!( + out, + " many refusals was waiting for the consumer, not for the tail." + ); +} + +/// Append one regime's table to `out`. +/// +/// Takes the buffer rather than printing, for the reason `pool_growth`'s twin +/// records: a helper writing to stdout while its caller composes a string emits +/// its lines first, reordering the report without losing any of it. +fn render_table(out: &mut dyn std::fmt::Write, runs: &[Run]) { + let _ = writeln!( + out, + "{:<18} {:>10} {:>14} {:>16} {:>14}", + "shape", "producers", "ns/push", "pushes/sec", "refusals" + ); + for run in runs { + let _ = writeln!( + out, + "{:<18} {:>10} {:>14.1} {:>16.0} {:>14}", + run.shape, run.producers, run.nanos_per_push, run.pushes_per_second, run.refusals + ); + } +} + +fn format_scaling(scaling: Option) -> String { + scaling.map_or_else(|| "--".to_owned(), |value| format!("{value:.2}x")) +} + +/// `numerator / denominator` as a cost ratio, or `--` when either is missing. +/// +/// Guards the denominator rather than trusting it: a shape that failed to run +/// reports zero, and a division by it would print `inf` or `NaN` in a column a +/// reader would otherwise take for a measurement. +fn format_ratio(numerator: Option, denominator: Option) -> String { + match (numerator, denominator) { + (Some(numerator), Some(denominator)) if denominator.nanos_per_push > 0.0 => { + format!( + "{:.2}x", + numerator.nanos_per_push / denominator.nanos_per_push + ) + } + _ => "--".to_owned(), + } +} + +fn format_nanos(run: Option) -> String { + run.map_or_else( + || "--".to_owned(), + |run| format!("{:.1}", run.nanos_per_push), + ) +} diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index a507d54f7..3972b0373 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -148,6 +148,7 @@ pub mod ioring; pub mod long_path; pub mod long_path_report; pub mod pool_growth; +pub mod queue_contention; pub mod report; /// The report oracle. **Test-support: present only where it is used.** /// diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs new file mode 100644 index 000000000..15d0e4deb --- /dev/null +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -0,0 +1,641 @@ +// Copyright (c) Mike Grier. + +//! Does the array queue's tail claim contend at realistic producer counts? +//! +//! **An experiment, not a component.** These probes measure platform behaviour +//! and are not for production use. Do not call them from production code, and +//! do not lift a technique out of here. See this crate's DESIGN-NOTES.md. +//! +//! # The two decisions this exists to force +//! +//! **1. Are the linked and sharded MPSC shapes needed at all?** They are parked +//! in `CHECKLIST-io-domains.md` as `M-inf.1`, gated on this measurement rather +//! than on taste. If N threads compare-and-swapping one tail does not collapse +//! at the producer counts a real system reaches, the bounded array queue is the +//! only MPSC the queue crate ever needs, and two speculative shapes never get +//! written. +//! +//! **2. Should `slotwise_mpsc` and `reserving_mpsc` merge?** They ship as peers because +//! honouring a reservation costs the producer a read of the consumer's +//! position -- one line every thread touches -- and *how much* that costs was a +//! judgement rather than a measurement. If it is cheap, the two shapes merge and +//! the non-reserving one goes; if it is expensive, the split is vindicated. +//! +//! # Two regimes, because one of them cannot answer the second question +//! +//! Producers are timed twice, and the pair is the point. +//! +//! - **Isolated** -- capacity large enough that nothing is ever refused, and no +//! consumer running. This is the *cleanest* measurement of tail-claim +//! contention: nothing else touches the queue, so whatever curve appears +//! against N is the compare-and-swap and nothing else. +//! +//! - **Drained** -- a consumer popping continuously while the producers push. +//! This is the one that can price `reserving_mpsc`, because its producer reads +//! `head`, and `head` is only expensive to read when a consumer is *writing* +//! it. Measured in isolation that read hits a clean, shared line and looks +//! free -- which would be a confident wrong answer. +//! +//! # What is deliberately not claimed +//! +//! The drained regime has a **single** consumer, because that is what MPSC +//! means. At high producer counts it is therefore expected to become +//! consumer-bound, and a throughput plateau there says nothing about the tail +//! claim. The probe reports each run's refusal count -- from the queue's own +//! `Observable` counters -- so a backpressure-bound run is visible as a fact +//! rather than mistaken for contention. Read the isolated regime for the +//! contention question, and the drained one for the cost of `head`. + +use std::sync::Arc; +use std::sync::Barrier; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::thread; +use std::time::Instant; + +use windows_waitable_queues::{permit_mpsc, reserving_mpsc, slotwise_mpsc}; + +use windows_waitable_queues::reserving_mpsc::{Balanced, ClaimLayout, Enduring, Perpetual, Wide}; + +/// How many pushes each producer thread performs in one timed run. +const PUSHES_PER_PRODUCER: usize = 50_000; + +/// How many times each configuration is repeated; the median is reported. +/// +/// Odd, so the median is an observed value rather than an average of two. Five +/// because these probes run on a virtual machine, where a single run can be +/// perturbed by something entirely outside the process. +const REPETITIONS: usize = 5; + +/// The producer counts measured, in order. +/// +/// Fixed rather than derived from the host's processor count, so two runs on +/// different machines produce comparable rows. The host's own count is reported +/// alongside, since the interesting region is around and beyond it. +pub const PRODUCER_COUNTS: &[usize] = &[1, 2, 4, 8, 16, 32]; + +/// The names a run is filed under. +/// +/// **Named once because a lookup by string literal is a rename waiting to +/// fail, and this one already did.** The `mpsc` -> `slotwise_mpsc` rename +/// updated the recording side and not the reporting binary, which went on +/// asking for `"mpsc"`; every lookup returned `None` and two entire columns of +/// the report rendered as `--` without anything erroring. A wrong shape name is +/// not a compile error, so the only defence is that both sides read the same +/// definition. +pub mod shapes { + /// The bounded-array MPSC. + pub const SLOTWISE_MPSC: &str = "slotwise_mpsc"; + /// The reservation-based MPSC. + pub const RESERVING_MPSC: &str = "reserving_mpsc"; + /// The experimental permit-claiming MPSC, measured against + /// [`RESERVING_MPSC`] because it is a candidate replacement for it. + pub const PERMIT_MPSC: &str = "permit_mpsc"; + /// The uncontended-atomic floor the queues are measured against. + pub const BASELINE_FETCH_ADD: &str = "baseline_fetch_add"; + /// `reserving_mpsc` on its default layout: a `u64` split 32 / 32. + /// + /// The same configuration as [`RESERVING_MPSC`], run again under its own + /// name so the layout comparison reads without a reader having to know + /// which layout the default is. + pub const CLAIM_NARROW: &str = "reserving(32/32)"; + /// `reserving_mpsc` on `Enduring`: a `u64` split 16 / 48. + pub const CLAIM_DEEP: &str = "reserving(16/48)"; + /// `reserving_mpsc` on `Perpetual`: a `u64` split 8 / 56. + pub const CLAIM_PERPETUAL: &str = "reserving(8/56)"; + /// `reserving_mpsc` on `Wide`: a `u128` split 64 / 64. + pub const CLAIM_WIDE: &str = "reserving(64/64)"; +} +/// One configuration's result. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Run { + /// Which queue shape, or the baseline. + pub shape: &'static str, + /// How many producer threads pushed concurrently. + pub producers: usize, + /// Median nanoseconds per successful push, across all producers. + pub nanos_per_push: f64, + /// Successful pushes per second, summed across producers. + pub pushes_per_second: f64, + /// Pushes refused for want of room during the median run. + /// + /// Non-zero means the run was at least partly bounded by the consumer + /// rather than by the claim, which is a fact about the measurement and not + /// about the queue. + pub refusals: u64, +} + +/// Everything one invocation measured. +#[derive(Debug, Clone)] +pub struct Observation { + /// Producers timed with no consumer and no possibility of refusal. + pub isolated: Vec, + /// Producers timed against a continuously draining consumer. + pub drained: Vec, + /// Logical processors the host reports. + pub logical_processors: usize, +} + +impl Observation { + /// Look one run up. + #[must_use] + pub fn find(&self, regime: &[Run], shape: &str, producers: usize) -> Option { + regime + .iter() + .find(|run| run.shape == shape && run.producers == producers) + .copied() + } + + /// How far throughput scaled from one producer to `producers`. + /// + /// 1.0 means N producers together push no faster than one did, which is + /// what a badly contended claim looks like. Perfect scaling would be N, + /// which no shared-tail queue can reach. + #[must_use] + pub fn scaling(&self, regime: &[Run], shape: &str, producers: usize) -> Option { + let one = self.find(regime, shape, 1)?; + let many = self.find(regime, shape, producers)?; + Some(many.pushes_per_second / one.pushes_per_second) + } +} + +/// Time every configuration. +#[must_use] +pub fn measure() -> Observation { + let mut isolated = Vec::new(); + let mut drained = Vec::new(); + + for &producers in PRODUCER_COUNTS { + isolated.push(median_run(shapes::BASELINE_FETCH_ADD, producers, |count| { + time_contended_atomic(count) + })); + isolated.push(median_run(shapes::SLOTWISE_MPSC, producers, |count| { + time_isolated_mpsc(count) + })); + isolated.push(median_run(shapes::RESERVING_MPSC, producers, |count| { + time_isolated_reserving(count) + })); + isolated.push(median_run(shapes::PERMIT_MPSC, producers, |count| { + time_isolated_permit(count) + })); + + drained.push(median_run(shapes::SLOTWISE_MPSC, producers, |count| { + time_drained_mpsc(count) + })); + drained.push(median_run(shapes::RESERVING_MPSC, producers, |count| { + time_drained_reserving(count) + })); + drained.push(median_run(shapes::PERMIT_MPSC, producers, |count| { + time_drained_permit(count) + })); + + isolated.push(median_run(shapes::CLAIM_NARROW, producers, |count| { + time_isolated_layout::(count) + })); + isolated.push(median_run(shapes::CLAIM_DEEP, producers, |count| { + time_isolated_layout::(count) + })); + isolated.push(median_run(shapes::CLAIM_PERPETUAL, producers, |count| { + time_isolated_layout::(count) + })); + isolated.push(median_run(shapes::CLAIM_WIDE, producers, |count| { + time_isolated_layout::(count) + })); + + drained.push(median_run(shapes::CLAIM_NARROW, producers, |count| { + time_drained_layout::(count) + })); + drained.push(median_run(shapes::CLAIM_DEEP, producers, |count| { + time_drained_layout::(count) + })); + drained.push(median_run(shapes::CLAIM_PERPETUAL, producers, |count| { + time_drained_layout::(count) + })); + drained.push(median_run(shapes::CLAIM_WIDE, producers, |count| { + time_drained_layout::(count) + })); + } + + Observation { + isolated, + drained, + logical_processors: thread::available_parallelism().map_or(0, std::num::NonZeroUsize::get), + } +} + +/// Raw result of one timed repetition: elapsed nanoseconds and refusals. +type Repetition = (f64, u64); + +/// Run one configuration [`REPETITIONS`] times and keep the median. +/// +/// The median rather than the mean, because on a virtual machine the failure +/// mode is one run being hugely slower rather than a spread around a centre, +/// and a mean would carry that outlier into the reported number. +fn median_run( + shape: &'static str, + producers: usize, + mut timer: impl FnMut(usize) -> Repetition, +) -> Run { + // One untimed pass first: the first touch of a fresh allocation faults + // pages in, and that cost belongs to the allocator rather than the queue. + let _ = timer(producers); + + let mut results: Vec = (0..REPETITIONS).map(|_| timer(producers)).collect(); + results.sort_by(|left, right| left.0.total_cmp(&right.0)); + let (elapsed_nanos, refusals) = results[REPETITIONS / 2]; + + let pushes = (producers * PUSHES_PER_PRODUCER) as f64; + Run { + shape, + producers, + nanos_per_push: elapsed_nanos / pushes, + pushes_per_second: pushes / (elapsed_nanos / 1e9), + refusals, + } +} + +/// The floor: N threads incrementing one shared counter. +/// +/// Not a queue, and not trying to be. It is the cheapest possible operation on +/// a contended line, so it says how much of a queue's scaling curve is the +/// queue and how much is simply what this processor does when N cores fight +/// over one cache line. +fn time_contended_atomic(producers: usize) -> Repetition { + let counter = Arc::new(AtomicU64::new(0)); + // One party per worker plus this thread. Every worker is created, then waits + // here; the clock starts as the barrier releases, so neither thread creation + // nor a solo head start by an early worker is inside the measurement. See + // `start_barrier`'s note for why that matters at these producer counts. + let gate = Arc::new(Barrier::new(producers + 1)); + let started = thread::scope(|scope| { + for _ in 0..producers { + let counter = Arc::clone(&counter); + let gate = Arc::clone(&gate); + scope.spawn(move || { + gate.wait(); + for _ in 0..PUSHES_PER_PRODUCER { + counter.fetch_add(1, Ordering::Relaxed); + } + }); + } + gate.wait(); + Instant::now() + }); + (started.elapsed().as_nanos() as f64, 0) +} + +/// Capacity big enough that a whole run fits, so nothing is ever refused. +fn capacity_for(producers: usize) -> usize { + (producers * PUSHES_PER_PRODUCER).next_power_of_two() +} + +/// A gate holding every participant until all of them exist. +/// +/// **Without this the row labelled N producers need not have measured N of +/// them.** Spawning is not instant, and each worker used to start pushing the +/// moment it was created, so at 50,000 pushes an early producer could complete +/// a long uncontended prefix -- or finish entirely -- before the last thread was +/// spawned. The reported interval also began before any worker existed, folding +/// thread-creation cost into a per-push number. The curve against N is the whole +/// output of this probe, and both effects bend it downward exactly where it is +/// steepest. +/// +/// The count includes this thread: the workers arrive and block, this thread +/// arrives last, and the clock starts as the barrier releases them together. +fn start_barrier(participants: usize) -> Arc { + Arc::new(Barrier::new(participants + 1)) +} + +fn time_isolated_mpsc(producers: usize) -> Repetition { + let (tx, rx) = + slotwise_mpsc::bounded::(capacity_for(producers)).expect("a valid capacity"); + let gate = start_barrier(producers); + let started = thread::scope(|scope| { + for producer in 0..producers { + let tx = tx.clone(); + let gate = Arc::clone(&gate); + scope.spawn(move || { + gate.wait(); + for index in 0..PUSHES_PER_PRODUCER { + tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) + .expect("the run fits in the capacity"); + } + }); + } + gate.wait(); + Instant::now() + }); + let elapsed = started.elapsed().as_nanos() as f64; + let refusals = tx.refused(); + // Drain before dropping: teardown would otherwise walk every slot, and that + // is not part of what is being timed. + while rx.pop().is_ok() {} + (elapsed, refusals) +} + +fn time_isolated_reserving(producers: usize) -> Repetition { + let (tx, rx) = + reserving_mpsc::bounded::(capacity_for(producers)).expect("a valid capacity"); + let gate = start_barrier(producers); + let started = thread::scope(|scope| { + for producer in 0..producers { + let tx = tx.clone(); + let gate = Arc::clone(&gate); + scope.spawn(move || { + gate.wait(); + for index in 0..PUSHES_PER_PRODUCER { + tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) + .expect("the run fits in the capacity"); + } + }); + } + gate.wait(); + Instant::now() + }); + let elapsed = started.elapsed().as_nanos() as f64; + let refusals = tx.refused(); + while rx.pop().is_ok() {} + (elapsed, refusals) +} + +/// The experimental permit claim, in the regime that isolates the claim itself. +/// +/// A line-for-line twin of [`time_isolated_reserving`] with one shape +/// substituted. Deliberately not factored into a generic over the two, which +/// would need a trait both implement and would put a dynamic or monomorphised +/// indirection inside the timed region -- in a measurement whose whole output is +/// a difference of a few nanoseconds per push. +fn time_isolated_permit(producers: usize) -> Repetition { + let (tx, rx) = permit_mpsc::bounded::(capacity_for(producers)).expect("a valid capacity"); + let gate = start_barrier(producers); + let started = thread::scope(|scope| { + for producer in 0..producers { + let tx = tx.clone(); + let gate = Arc::clone(&gate); + scope.spawn(move || { + gate.wait(); + for index in 0..PUSHES_PER_PRODUCER { + tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) + .expect("the run fits in the capacity"); + } + }); + } + gate.wait(); + Instant::now() + }); + let elapsed = started.elapsed().as_nanos() as f64; + let refusals = tx.refused(); + while rx.pop().is_ok() {} + (elapsed, refusals) +} + +/// A capacity a real system would choose, so the drained regime exercises +/// backpressure the way a real one would. +const DRAINED_CAPACITY: usize = 1024; + +fn time_drained_mpsc(producers: usize) -> Repetition { + let (tx, rx) = slotwise_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); + let done = Arc::new(AtomicBool::new(false)); + let consumer_done = Arc::clone(&done); + // The consumer is a participant too: it is spawned first, but spawning is + // not readiness, and a consumer still starting up while producers push turns + // the opening of the run into an undrained regime -- the one thing this + // measurement is defined against. + let gate = start_barrier(producers + 1); + let consumer_gate = Arc::clone(&gate); + + let consumer = thread::spawn(move || { + consumer_gate.wait(); + // Spin rather than park: the doorbell's cost is `doorbell_cost`'s + // question, and parking here would measure that instead of the claim. + while !consumer_done.load(Ordering::Relaxed) { + while rx.pop().is_ok() {} + std::hint::spin_loop(); + } + while rx.pop().is_ok() {} + rx.refused() + }); + + let started = thread::scope(|scope| { + for producer in 0..producers { + let tx = tx.clone(); + let gate = Arc::clone(&gate); + scope.spawn(move || { + gate.wait(); + for index in 0..PUSHES_PER_PRODUCER { + let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; + // Retry on a full queue, which is what a real producer + // does. The refusal count is what makes that visible. + while let Err(error) = tx.push(item) { + item = error.into_inner(); + std::hint::spin_loop(); + } + } + }); + } + gate.wait(); + Instant::now() + }); + let elapsed = started.elapsed().as_nanos() as f64; + + done.store(true, Ordering::Relaxed); + drop(tx); + let refusals = consumer.join().expect("the consumer must not panic"); + (elapsed, refusals) +} + +fn time_drained_reserving(producers: usize) -> Repetition { + // **Defaults on both sides, and that is a correction.** This row previously + // enabled high-water tracking here and nowhere else, to "also price the + // switch M31.4 made opt-in". But the number it feeds is presented as the + // cost of *reservation*, and tracking adds an unrelated operation to this + // shape's push path alone -- a load of the consumer's position, which is + // exactly the shared line the other shape's push is built to avoid + // touching. The ratio therefore measured reservation plus a handicap, with + // no way for a reader to separate them. + // + // Nothing consumes the high-water figure here either, so the tracking was + // paying a cost to produce a number nobody read. Pricing that switch is a + // worthwhile measurement and needs its own row, with both shapes tracking, + // rather than being folded into this comparison. + let (tx, rx) = reserving_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); + let done = Arc::new(AtomicBool::new(false)); + let consumer_done = Arc::clone(&done); + // The consumer joins the gate here for the reason it does in the slotwise + // twin: a run whose opening is undrained is not the regime being measured. + let gate = start_barrier(producers + 1); + let consumer_gate = Arc::clone(&gate); + + let consumer = thread::spawn(move || { + consumer_gate.wait(); + while !consumer_done.load(Ordering::Relaxed) { + while rx.pop().is_ok() {} + std::hint::spin_loop(); + } + while rx.pop().is_ok() {} + rx.refused() + }); + + let started = thread::scope(|scope| { + for producer in 0..producers { + let tx = tx.clone(); + let gate = Arc::clone(&gate); + scope.spawn(move || { + gate.wait(); + for index in 0..PUSHES_PER_PRODUCER { + let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; + while let Err(error) = tx.push(item) { + item = error.into_inner(); + std::hint::spin_loop(); + } + } + }); + } + gate.wait(); + Instant::now() + }); + let elapsed = started.elapsed().as_nanos() as f64; + + done.store(true, Ordering::Relaxed); + drop(tx); + let refusals = consumer.join().expect("the consumer must not panic"); + (elapsed, refusals) +} + +/// The experimental permit claim, against a continuously draining consumer. +/// +/// The regime that can price the claim honestly, for the same reason the +/// reserving twin needs it: the shared line a producer touches is only +/// expensive when a consumer is writing it. Measured in isolation, an +/// uncontended line looks free -- which would be a confident wrong answer, and +/// this shape has more riding on that answer than the others, because it trades +/// `reserving_mpsc`'s *load* of the consumer's position for a read-modify-write +/// on a count the consumer also writes. +fn time_drained_permit(producers: usize) -> Repetition { + let (tx, rx) = permit_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); + let done = Arc::new(AtomicBool::new(false)); + let consumer_done = Arc::clone(&done); + let gate = start_barrier(producers + 1); + let consumer_gate = Arc::clone(&gate); + + let consumer = thread::spawn(move || { + consumer_gate.wait(); + while !consumer_done.load(Ordering::Relaxed) { + while rx.pop().is_ok() {} + std::hint::spin_loop(); + } + while rx.pop().is_ok() {} + rx.refused() + }); + + let started = thread::scope(|scope| { + for producer in 0..producers { + let tx = tx.clone(); + let gate = Arc::clone(&gate); + scope.spawn(move || { + gate.wait(); + for index in 0..PUSHES_PER_PRODUCER { + let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; + while let Err(error) = tx.push(item) { + item = error.into_inner(); + std::hint::spin_loop(); + } + } + }); + } + gate.wait(); + Instant::now() + }); + let elapsed = started.elapsed().as_nanos() as f64; + + done.store(true, Ordering::Relaxed); + drop(tx); + let refusals = consumer.join().expect("the consumer must not panic"); + (elapsed, refusals) +} + +/// One claim-word layout, in the regime that isolates the claim. +/// +/// **Generic over the layout, where [`time_isolated_permit`] is deliberately +/// duplicated, and the difference is the point.** That twin compares two +/// *different types*, which a generic could only unify behind a trait, putting +/// an indirection that might not inline identically inside the timed region. +/// These are the *same type* at different layout parameters, so this +/// monomorphises to exactly the code a hand-written copy would produce -- there +/// is nothing left to dispatch. +/// +/// Measures `reserving_mpsc` itself rather than a stand-in. An earlier form of +/// this probe carried its own duplicated implementation of the claim protocol, +/// built so the layouts could be compared before the shipping crate had them; +/// it drifted from the original twice while doing so. The shipping type takes +/// the layout as a parameter now, so the duplicate is gone. +fn time_isolated_layout(producers: usize) -> Repetition { + let (tx, rx) = + reserving_mpsc::bounded_as::(capacity_for(producers)).expect("a valid capacity"); + let gate = start_barrier(producers); + let started = thread::scope(|scope| { + for producer in 0..producers { + let tx = tx.clone(); + let gate = Arc::clone(&gate); + scope.spawn(move || { + gate.wait(); + for index in 0..PUSHES_PER_PRODUCER { + tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) + .expect("the run fits in the capacity"); + } + }); + } + gate.wait(); + Instant::now() + }); + let elapsed = started.elapsed().as_nanos() as f64; + let refusals = tx.refused(); + while rx.pop().is_ok() {} + (elapsed, refusals) +} + +/// One claim-word layout, against a continuously draining consumer. +/// +/// Generic for [`time_isolated_layout`]'s reason. +fn time_drained_layout(producers: usize) -> Repetition { + let (tx, rx) = + reserving_mpsc::bounded_as::(DRAINED_CAPACITY).expect("a valid capacity"); + let done = Arc::new(AtomicBool::new(false)); + let consumer_done = Arc::clone(&done); + // The consumer joins the gate for the reason its twins do: a run whose + // opening is undrained is not the regime being measured. + let gate = start_barrier(producers + 1); + let consumer_gate = Arc::clone(&gate); + + let consumer = thread::spawn(move || { + consumer_gate.wait(); + while !consumer_done.load(Ordering::Relaxed) { + while rx.pop().is_ok() {} + std::hint::spin_loop(); + } + while rx.pop().is_ok() {} + rx.refused() + }); + + let started = thread::scope(|scope| { + for producer in 0..producers { + let tx = tx.clone(); + let gate = Arc::clone(&gate); + scope.spawn(move || { + gate.wait(); + for index in 0..PUSHES_PER_PRODUCER { + let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; + while let Err(error) = tx.push(item) { + item = error.into_inner(); + std::hint::spin_loop(); + } + } + }); + } + gate.wait(); + Instant::now() + }); + let elapsed = started.elapsed().as_nanos() as f64; + done.store(true, Ordering::Relaxed); + let refusals = consumer.join().expect("the consumer must not panic"); + (elapsed, refusals) +} From d74a00cea18efc05609a5751016ae21803a68753 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 15:55:06 -0400 Subject: [PATCH 002/139] fix(platform-probes): restore the default-features build the probe's features took away Four findings from a code review of this PR, one of them a real regression the PR introduced. **Enabling `dwcas` and `experimental-permit-claim` on a workspace member unifies them across the whole workspace.** `windows-platform-probes` did not depend on `windows-waitable-queues` at all before this branch, so every `--workspace` step built that crate at its default features. With the probe's dependency added, `cargo tree --workspace -e features -i windows-waitable-queues` now reports both features on, sourced solely from here -- and since CI builds that crate only through `--workspace` steps, with no `-p windows-waitable-queues` job anywhere, NOTHING was left compiling it without `dwcas`. That is the configuration that can break unnoticed, because `dwcas` is additive: `Wide` and its `ClaimLayout` impl exist only under it. The queue crate's own manifest calls `dwcas` "non-default so nothing depends on it by accident" and "the only thing in this crate that costs a third-party dependency" -- claims that are only true while something still builds without it. Fixed by adding a `windows-waitable-queues (default features)` job, modelled on the existing `placement-probe-no-serde` job, which exists for exactly this shape of problem. Verified locally before committing: build, clippy `-D warnings`, and 304 + 12 + 1 tests all clean at default features. Not fixed by making the probe's dependency optional: the probe must stay buildable by a plain `cargo build`, and a feature that has to be remembered before it compiles is a worse trade than a job that cannot be forgotten. **The design note's stated reason for keeping the probe out of CI was false**, and it was the load-bearing half. It said the job "runs `cargo run` without `--release`" -- but `probe-doorbell-cost` and `probe-request-cost` already run there WITH `--release`, under a comment establishing the very rule this probe would fall under. It also said "unlike every other probe", and `probe-cancel-io` is likewise absent. And it said the run "costs about two minutes" against the 64.8s this PR's own commit message reports. The decision is unchanged and still right; the argument for it was wrong. Rewritten around what is true -- core count and wall time -- with the release requirement kept as a constraint on HOW it runs rather than as a reason to exclude it. Re-measured: 60.2s. **Two prose defects, both mine and both the same class as ones this PR already fixed elsewhere.** The module doc still named `CHECKLIST-io-domains.md`, a file in no branch -- I swept the design notes for dangling links and did not sweep the source beside them. And three sites named `mpsc`, a module the queue crate does not have; it is `slotwise_mpsc`, which is what the probe measures and labels. That is residue of exactly the rename this probe's own comments record as having caused one silent failure already. Re-ran the probe after changing its output text rather than assuming: exit 0, 60.2s, 181 lines, and the reworded passage renders as intended. Swept every path-shaped `.md`/`.rs` reference in the PR's files for resolvability: zero unresolvable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 39 +++++++++++++++++++ .../windows-platform-probes/DESIGN-NOTES.md | 30 ++++++++------ .../src/bin/queue_contention.rs | 13 ++++--- .../src/queue_contention.rs | 7 +++- 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 117c87126..6687e07d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -500,6 +500,45 @@ jobs: RUSTDOCFLAGS: "-D rustdoc::broken_intra_doc_links -D rustdoc::private_intra_doc_links" run: cargo doc -p windows-placement-probe --no-deps --no-default-features --locked + waitable-queues-default-features: + name: windows-waitable-queues (default features) + runs-on: windows-latest + # **This job exists because adding `probe-queue-contention` took the default + # configuration away from every other job.** That probe needs `dwcas` and + # `experimental-permit-claim`, and enabling them on a workspace member + # unifies them across the whole workspace -- so the `--workspace` steps that + # deliberately omit `--all-features` stopped being a default-features build + # of THIS crate, and no job was left compiling it without `dwcas`. + # + # That matters because `dwcas` is additive: `Wide` and its `ClaimLayout` impl + # exist only under it, so the configuration that loses them is the one that + # can break unnoticed. The crate's manifest says `dwcas` is "non-default so + # nothing depends on it by accident" and is "the only thing in this crate + # that costs a third-party dependency" -- a claim that is only true while + # something still builds without it. + # + # Named per-crate rather than fixed by making the probe's dependency + # optional: the probe must stay buildable by a plain `cargo build`, and a + # feature that has to be remembered before the probe compiles is a worse + # trade than a job that cannot be forgotten. + env: + RUSTUP_TOOLCHAIN: stable + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - name: cargo build + run: cargo build -p windows-waitable-queues --all-targets --locked + - name: cargo clippy + run: cargo clippy -p windows-waitable-queues --all-targets --locked -- -D warnings + - name: cargo test + env: + RUST_BACKTRACE: 1 + RUST_LIB_BACKTRACE: 1 + run: cargo test -p windows-waitable-queues --locked --no-fail-fast + fmt: name: rustfmt runs-on: windows-latest diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 963e63f53..df76a25bd 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -493,17 +493,25 @@ rest of the queue work -- so this note deliberately names the QUESTIONS rather t that would dangle. The probe is the instrument; it is useful before the plan that consumes it lands, and it is landed first precisely so the decision is made against measurement rather than argument. -**It is deliberately absent from the `platform-probes` CI job, unlike every other probe, and the reason is -a measurement rather than a preference.** That job runs `cargo run` without `--release`. Measured in a -debug build, `mpsc` and `reserving_mpsc` come out at 249.7 and 254.0 ns/push at sixteen producers -- -indistinguishable. In release, on the same machine in the same minute, they are 193.5 and 52.2. The -un-inlined overhead of a debug build swamps the cache-coherence effects that *are* the finding, so a debug -run of this probe does not merely lose precision: it reports the two shapes as equivalent, which is a -confident wrong answer of exactly the kind this crate's `doorbell_cost` notes warn about. - -Two further reasons it stays out. A contention curve needs more cores than a hosted runner has, and the -32-producer rows on a four-core runner would measure the scheduler. And the run costs about two minutes in -release, against a job whose other probes are seconds. +**It is deliberately absent from the `platform-probes` CI job, and the reasons are a core count and a +clock rather than a preference.** A contention curve needs more cores than a hosted runner has: the +sixteen- and thirty-two-producer rows on a four-core runner would measure the scheduler and report it as +contention. And the run costs about 65 seconds, against a job whose other probes are seconds apiece. + +**It must be run in release, which is a measurement and not a preference.** In a debug build +`slotwise_mpsc` and `reserving_mpsc` come out at 249.7 and 254.0 ns/push at sixteen producers -- +indistinguishable. In release, on the same machine in the same minute, 193.5 and 52.2. The un-inlined +overhead of a debug build swamps the cache-coherence effects that *are* the finding, so a debug run does +not merely lose precision: it reports the two shapes as equivalent, which is a confident wrong answer of +exactly the kind this crate's `doorbell_cost` notes warn about. + +**That is a constraint on HOW it runs, not an argument for keeping it out**, and an earlier draft of this +paragraph confused the two -- it said the CI job "runs `cargo run` without `--release`", which is not true +of the job it describes: `probe-doorbell-cost` and `probe-request-cost` already run there with `--release`, +under a comment establishing exactly the rule this probe would fall under. It also said "unlike every other +probe", and `probe-cancel-io` is likewise absent. Corrected by a review. The release precedent exists; what +keeps this one out is that it costs an order of magnitude more than the two probes that use it, on hardware +that cannot answer the question anyway. So this one is run by hand, on a known machine, and its numbers are recorded with the machine attached. diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index ce926ad49..7019e1ba8 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -7,8 +7,8 @@ //! do not lift a technique out of here. See this crate's DESIGN-NOTES.md. //! //! This decides two things that are otherwise decided by taste: whether the -//! linked and sharded MPSC shapes are ever needed, and whether `mpsc` and -//! `reserving_mpsc` should merge. See `queue_contention`'s module docs. +//! linked and sharded MPSC shapes are ever needed, and whether `slotwise_mpsc` +//! and `reserving_mpsc` should merge. See `queue_contention`'s module docs. use windows_platform_probes::queue_contention::{PRODUCER_COUNTS, Run, measure, shapes}; use windows_platform_probes::report::emit_report; @@ -132,13 +132,16 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " `mpsc` does not, which is the entire reason they ship as two" + " `slotwise_mpsc` does not, which is the entire reason they ship as" ); let _ = writeln!( out, - " shapes. This regime is the one that can price that read, because" + " two shapes. This regime is the one that can price that read," + ); + let _ = writeln!( + out, + " because a consumer is writing the line being read." ); - let _ = writeln!(out, " a consumer is writing the line being read."); let _ = writeln!( out, "\n `permit_mpsc` is experimental and is the candidate replacement" diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 15d0e4deb..f1020a055 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -9,8 +9,11 @@ //! # The two decisions this exists to force //! //! **1. Are the linked and sharded MPSC shapes needed at all?** They are parked -//! in `CHECKLIST-io-domains.md` as `M-inf.1`, gated on this measurement rather -//! than on taste. If N threads compare-and-swapping one tail does not collapse +//! in a plan that is not in this repository yet, gated on this measurement +//! rather than on taste. (The named checklist file arrives with the rest of the +//! queue work; naming a path that does not resolve is what this crate's own +//! link rule forbids, and an earlier draft did it here.) +//! If N threads compare-and-swapping one tail does not collapse //! at the producer counts a real system reaches, the bounded array queue is the //! only MPSC the queue crate ever needs, and two speculative shapes never get //! written. From d49a71f2225d9dc5d406eea24e5a8b39c9a7fed3 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 16:51:51 -0400 Subject: [PATCH 003/139] fix(platform-probes): time the producers, not this thread, and refuse a spin on a dead queue Three findings from a second review of this PR. The first changes the numbers. **The timing window was wrong at both ends, and the error was not small.** Every timer released a barrier, called `Instant::now()` on THIS thread, and read `elapsed()` after `thread::scope` returned: - `Barrier::wait` releases every party together and this thread is just another party, so a worker could return from `wait` and run an arbitrary prefix of its pushes before this thread was scheduled again to read the clock. That understates the interval, which OVERSTATES throughput -- and it bites hardest at high producer counts, where this thread is competing with N busy workers for a core. Which is exactly where the curve is the finding. - `thread::scope` joins before it returns, so thread exit and join sat inside the measured interval. That overstates it, and dominates at one producer where a whole run is only hundreds of microseconds. Each worker now timestamps itself either side of its own pushes, and the span is the earliest start to the latest finish. A previous review had explicitly cleared this, concluding the clock "genuinely starts at release"; it does not. **Measured, with a control, because a benchmark correction that is not measured is just a rewrite.** On x86_64 16p/8c, `reserving_mpsc` at sixteen producers: 35.0 ns/push before, 49.8-52.8 after. Two runs of the same build put the run-to-run spread at 2-6%, so this is signal: the probe was reporting roughly 45% more throughput than the producers actually achieved, at the producer counts the decision turns on. The one-producer rows moved the other way and slightly (2.4 -> 2.3, 6.5 -> 6.0), which is the join overhead leaving the window. The four figures recorded in DESIGN-NOTES predate this and are now marked as such rather than quietly left standing. The qualitative finding they support -- a debug build swamps the effect -- is unaffected. **A dead queue was retried forever.** The drained producers looped on every `PushError`, but only `Full` is retryable; the queue crate's own documentation says "retrying the first is sensible and retrying the second is a spin". If the consumer panicked and dropped the receiver, every producer would spin indefinitely, and because the consumer is joined only after the producer scope completes, the panic could never surface -- the probe would hang rather than fail. All four loops now assert `is_retryable()` first. Joining the producers inside the scope, which the timing fix required anyway, means a producer panic now surfaces too. **The isolated regime does not isolate the compare-and-swap**, and both the module doc and the design note said it did: "whatever curve appears against N is the compare-and-swap and nothing else". What is timed is each shape's whole push path -- tail claim, slot-sequence load, item write, publication store, doorbell fence -- and `permit_mpsc` takes two shared read-modify-writes where the others take one. A difference here is a difference in push cost. Narrowed to that. Gate: fmt, clippy --all-targets, 236 lib + 12 integration, and the probe run end to end (exit 0, 63.6s and 64.8s across two runs). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 14 +- .../src/queue_contention.rs | 234 ++++++++++++++---- 2 files changed, 195 insertions(+), 53 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index df76a25bd..097c2146e 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -505,6 +505,15 @@ overhead of a debug build swamps the cache-coherence effects that *are* the find not merely lose precision: it reports the two shapes as equivalent, which is a confident wrong answer of exactly the kind this crate's `doorbell_cost` notes warn about. +**Those four figures predate a correction to the timing window and have not been retaken.** The +qualitative finding is unaffected -- a debug build still swamps the effect -- but the numbers themselves +were measured while the probe timed from this thread's clock rather than from the producers' own, which +overstated throughput at high producer counts. Measured on `x86_64 16p/8c` after the correction, with a +second run of the same build as the noise control: `reserving_mpsc` at sixteen producers moved from +35.0 to 49.8-52.8 ns/push, against a run-to-run spread of 2-6%. So the correction is worth roughly 45% +at the producer counts where the curve is the finding, and any figure in this note taken before it +should be read as optimistic until retaken on a known host. + **That is a constraint on HOW it runs, not an argument for keeping it out**, and an earlier draft of this paragraph confused the two -- it said the CI job "runs `cargo run` without `--release`", which is not true of the job it describes: `probe-doorbell-cost` and `probe-request-cost` already run there with `--release`, @@ -520,7 +529,10 @@ So this one is run by hand, on a known machine, and its numbers are recorded wit Two regimes, and the pair is the point. **Isolated** gives producers a capacity large enough that nothing is ever refused and runs no consumer, so -whatever curve appears against N is the claim and nothing else. **Drained** runs a consumer popping +whatever curve appears against N is the producer side alone, with no consumer traffic in it. It is not +the claim alone -- what is timed is each shape's whole push path, tail claim and slot write and +publication and doorbell together, so a difference here is a difference in PUSH COST rather than +evidence about the claim on its own. **Drained** runs a consumer popping continuously, which is the only regime that can price `reserving_mpsc`'s read of `head` -- that read is cheap until a consumer is *writing* the line, and measuring it in isolation would report it as free. diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index f1020a055..d917f0324 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -29,9 +29,16 @@ //! Producers are timed twice, and the pair is the point. //! //! - **Isolated** -- capacity large enough that nothing is ever refused, and no -//! consumer running. This is the *cleanest* measurement of tail-claim -//! contention: nothing else touches the queue, so whatever curve appears -//! against N is the compare-and-swap and nothing else. +//! consumer running. Nothing else touches the queue, so the curve against N +//! is the producer side alone, with no consumer traffic in it. +//! +//! **It is not the compare-and-swap alone, and an earlier draft said it +//! was.** What is timed is each shape's whole push path: the tail claim, but +//! also the slot-sequence load, the item write, the publication store, and +//! the doorbell's fence. `permit_mpsc` takes two shared read-modify-writes +//! where the others take one. So a difference between shapes here is a +//! difference in PUSH COST, and attributing it to the claim alone would be +//! reading more out of the number than is in it. Found by a review. //! //! - **Drained** -- a consumer popping continuously while the producers push. //! This is the one that can price `reserving_mpsc`, because its producer reads @@ -269,21 +276,27 @@ fn time_contended_atomic(producers: usize) -> Repetition { // nor a solo head start by an early worker is inside the measurement. See // `start_barrier`'s note for why that matters at these producer counts. let gate = Arc::new(Barrier::new(producers + 1)); - let started = thread::scope(|scope| { + let spans = thread::scope(|scope| { + let mut workers = Vec::with_capacity(producers); for _ in 0..producers { let counter = Arc::clone(&counter); let gate = Arc::clone(&gate); - scope.spawn(move || { + workers.push(scope.spawn(move || { gate.wait(); + let began = Instant::now(); for _ in 0..PUSHES_PER_PRODUCER { counter.fetch_add(1, Ordering::Relaxed); } - }); + (began, Instant::now()) + })); } gate.wait(); - Instant::now() + workers + .into_iter() + .map(|worker| worker.join().expect("a producer must not panic")) + .collect::>() }); - (started.elapsed().as_nanos() as f64, 0) + (measured_span(&spans), 0) } /// Capacity big enough that a whole run fits, so nothing is ever refused. @@ -302,32 +315,75 @@ fn capacity_for(producers: usize) -> usize { /// output of this probe, and both effects bend it downward exactly where it is /// steepest. /// -/// The count includes this thread: the workers arrive and block, this thread -/// arrives last, and the clock starts as the barrier releases them together. +/// The count includes this thread, so no worker can start before the last one +/// exists. It does NOT start the clock -- see [`measured_span`] for why that is +/// a separate job. fn start_barrier(participants: usize) -> Arc { Arc::new(Barrier::new(participants + 1)) } +/// The wall-clock window the producers were actually inside: from the first to +/// begin to the last to finish. +/// +/// **Each worker times itself, because this thread cannot time them.** The +/// obvious arrangement -- release the barrier, call `Instant::now()` here, and +/// read `elapsed()` after the scope ends -- is wrong at both ends, and a review +/// caught it: +/// +/// - `Barrier::wait` releases every party together, and this thread is just +/// another party. A worker can return from `wait` and run an arbitrary prefix +/// of its pushes before this thread is scheduled again to read the clock, so +/// the start could land after work had already happened. That understates the +/// interval, which OVERSTATES throughput. +/// - `thread::scope` joins every worker before it returns, so an `elapsed()` +/// read after it includes thread exit and join. That overstates the interval, +/// which understates throughput. +/// +/// Neither error is bounded by anything this probe controls, and both bite +/// hardest on the fast low-producer rows where a run is only hundreds of +/// microseconds. Taking the earliest start and the latest finish measures the +/// span the producers were contending over and nothing else. +fn measured_span(spans: &[(Instant, Instant)]) -> f64 { + let began = spans + .iter() + .map(|(began, _)| *began) + .min() + .expect("a run has at least one producer"); + let ended = spans + .iter() + .map(|(_, ended)| *ended) + .max() + .expect("a run has at least one producer"); + + ended.duration_since(began).as_nanos() as f64 +} + fn time_isolated_mpsc(producers: usize) -> Repetition { let (tx, rx) = slotwise_mpsc::bounded::(capacity_for(producers)).expect("a valid capacity"); let gate = start_barrier(producers); - let started = thread::scope(|scope| { + let spans = thread::scope(|scope| { + let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); - scope.spawn(move || { + workers.push(scope.spawn(move || { gate.wait(); + let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) .expect("the run fits in the capacity"); } - }); + (began, Instant::now()) + })); } gate.wait(); - Instant::now() + workers + .into_iter() + .map(|worker| worker.join().expect("a producer must not panic")) + .collect::>() }); - let elapsed = started.elapsed().as_nanos() as f64; + let elapsed = measured_span(&spans); let refusals = tx.refused(); // Drain before dropping: teardown would otherwise walk every slot, and that // is not part of what is being timed. @@ -339,22 +395,28 @@ fn time_isolated_reserving(producers: usize) -> Repetition { let (tx, rx) = reserving_mpsc::bounded::(capacity_for(producers)).expect("a valid capacity"); let gate = start_barrier(producers); - let started = thread::scope(|scope| { + let spans = thread::scope(|scope| { + let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); - scope.spawn(move || { + workers.push(scope.spawn(move || { gate.wait(); + let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) .expect("the run fits in the capacity"); } - }); + (began, Instant::now()) + })); } gate.wait(); - Instant::now() + workers + .into_iter() + .map(|worker| worker.join().expect("a producer must not panic")) + .collect::>() }); - let elapsed = started.elapsed().as_nanos() as f64; + let elapsed = measured_span(&spans); let refusals = tx.refused(); while rx.pop().is_ok() {} (elapsed, refusals) @@ -370,22 +432,28 @@ fn time_isolated_reserving(producers: usize) -> Repetition { fn time_isolated_permit(producers: usize) -> Repetition { let (tx, rx) = permit_mpsc::bounded::(capacity_for(producers)).expect("a valid capacity"); let gate = start_barrier(producers); - let started = thread::scope(|scope| { + let spans = thread::scope(|scope| { + let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); - scope.spawn(move || { + workers.push(scope.spawn(move || { gate.wait(); + let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) .expect("the run fits in the capacity"); } - }); + (began, Instant::now()) + })); } gate.wait(); - Instant::now() + workers + .into_iter() + .map(|worker| worker.join().expect("a producer must not panic")) + .collect::>() }); - let elapsed = started.elapsed().as_nanos() as f64; + let elapsed = measured_span(&spans); let refusals = tx.refused(); while rx.pop().is_ok() {} (elapsed, refusals) @@ -418,27 +486,44 @@ fn time_drained_mpsc(producers: usize) -> Repetition { rx.refused() }); - let started = thread::scope(|scope| { + let spans = thread::scope(|scope| { + let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); - scope.spawn(move || { + workers.push(scope.spawn(move || { gate.wait(); + let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; - // Retry on a full queue, which is what a real producer - // does. The refusal count is what makes that visible. + // Retry a FULL queue, which is what a real producer does; + // the refusal count is what makes that visible. Anything + // else is not retryable -- a disconnected queue never + // drains -- and retrying it is an infinite spin that + // presents as a hung probe rather than as the consumer + // failure it actually is. The queue crate says so itself: + // "retrying the first is sensible and retrying the second + // is a spin". while let Err(error) = tx.push(item) { + assert!( + error.is_retryable(), + "the consumer is gone, so this push can never \ + succeed: {error}" + ); item = error.into_inner(); std::hint::spin_loop(); } } - }); + (began, Instant::now()) + })); } gate.wait(); - Instant::now() + workers + .into_iter() + .map(|worker| worker.join().expect("a producer must not panic")) + .collect::>() }); - let elapsed = started.elapsed().as_nanos() as f64; + let elapsed = measured_span(&spans); done.store(true, Ordering::Relaxed); drop(tx); @@ -478,25 +563,38 @@ fn time_drained_reserving(producers: usize) -> Repetition { rx.refused() }); - let started = thread::scope(|scope| { + let spans = thread::scope(|scope| { + let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); - scope.spawn(move || { + workers.push(scope.spawn(move || { gate.wait(); + let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; while let Err(error) = tx.push(item) { + // Only a FULL queue is retryable; see the note on the + // first of these loops. + assert!( + error.is_retryable(), + "the consumer is gone, so this push can never \ + succeed: {error}" + ); item = error.into_inner(); std::hint::spin_loop(); } } - }); + (began, Instant::now()) + })); } gate.wait(); - Instant::now() + workers + .into_iter() + .map(|worker| worker.join().expect("a producer must not panic")) + .collect::>() }); - let elapsed = started.elapsed().as_nanos() as f64; + let elapsed = measured_span(&spans); done.store(true, Ordering::Relaxed); drop(tx); @@ -530,25 +628,38 @@ fn time_drained_permit(producers: usize) -> Repetition { rx.refused() }); - let started = thread::scope(|scope| { + let spans = thread::scope(|scope| { + let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); - scope.spawn(move || { + workers.push(scope.spawn(move || { gate.wait(); + let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; while let Err(error) = tx.push(item) { + // Only a FULL queue is retryable; see the note on the + // first of these loops. + assert!( + error.is_retryable(), + "the consumer is gone, so this push can never \ + succeed: {error}" + ); item = error.into_inner(); std::hint::spin_loop(); } } - }); + (began, Instant::now()) + })); } gate.wait(); - Instant::now() + workers + .into_iter() + .map(|worker| worker.join().expect("a producer must not panic")) + .collect::>() }); - let elapsed = started.elapsed().as_nanos() as f64; + let elapsed = measured_span(&spans); done.store(true, Ordering::Relaxed); drop(tx); @@ -575,22 +686,28 @@ fn time_isolated_layout(producers: usize) -> Repetition { let (tx, rx) = reserving_mpsc::bounded_as::(capacity_for(producers)).expect("a valid capacity"); let gate = start_barrier(producers); - let started = thread::scope(|scope| { + let spans = thread::scope(|scope| { + let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); - scope.spawn(move || { + workers.push(scope.spawn(move || { gate.wait(); + let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) .expect("the run fits in the capacity"); } - }); + (began, Instant::now()) + })); } gate.wait(); - Instant::now() + workers + .into_iter() + .map(|worker| worker.join().expect("a producer must not panic")) + .collect::>() }); - let elapsed = started.elapsed().as_nanos() as f64; + let elapsed = measured_span(&spans); let refusals = tx.refused(); while rx.pop().is_ok() {} (elapsed, refusals) @@ -619,25 +736,38 @@ fn time_drained_layout(producers: usize) -> Repetition rx.refused() }); - let started = thread::scope(|scope| { + let spans = thread::scope(|scope| { + let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); - scope.spawn(move || { + workers.push(scope.spawn(move || { gate.wait(); + let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; while let Err(error) = tx.push(item) { + // Only a FULL queue is retryable; see the note on the + // first of these loops. + assert!( + error.is_retryable(), + "the consumer is gone, so this push can never \ + succeed: {error}" + ); item = error.into_inner(); std::hint::spin_loop(); } } - }); + (began, Instant::now()) + })); } gate.wait(); - Instant::now() + workers + .into_iter() + .map(|worker| worker.join().expect("a producer must not panic")) + .collect::>() }); - let elapsed = started.elapsed().as_nanos() as f64; + let elapsed = measured_span(&spans); done.store(true, Ordering::Relaxed); let refusals = consumer.join().expect("the consumer must not panic"); (elapsed, refusals) From 0ca3ea148b11bc545549949d83ace17441e319e9 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 18:52:35 -0400 Subject: [PATCH 004/139] docs(platform-probes): withdraw the "re-apportionment is free" claim The queue-contention section said the u64 re-apportionments "track the default within noise", so twenty years of counter headroom was "free". The table directly beneath it showed 1.21x and 1.13x against a noise floor the same file put at 2-6% -- the prose contradicted its own evidence in adjacent lines. Re-measured seven times on x86_64 16p/8c after the timing correction in d49a71f, which the original figures predate. Two findings: - The 2-6% floor was obtained by comparing two runs, which cannot measure a spread. Seven runs put the same-configuration spread at 7-61%. - The probe already emits its own control: `reserving_mpsc` and `reserving(32/32)` are the same code at the same layout, measured twice per run, so their ratio is an empirical "no difference" -- 0.68-1.27x. That is a derived control rather than an asserted floor. Against that control the 128-bit word separates decisively (3.45x/3.81x at 16/32 producers) and the u64 re-apportionments do not. The claim is withdrawn in BOTH directions rather than inverted: 1.23-1.30x against a control reaching 1.12x, on one host, is a flag to measure locally, not a cost. Swept the class rather than the reported line: the same claim appeared three times (the stand-in section, the headroom section's "at no measured cost", and the re-measured section). All three corrected. Records D-observations-not-verdicts: figures are published with their capture parameters, and fine-grained layout choices belong to the client. Windows exposes no NUMA distance table, so the processor-to-node assignment in the banner is the analog used in its place; the measurement host's `numa[16]` is a single domain, so these figures say nothing about cross-domain behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 201 ++++++++++++++---- .../DESIGN-RATIONALE.md | 56 +++++ 2 files changed, 220 insertions(+), 37 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 097c2146e..fe8cfe93f 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -19,6 +19,56 @@ concentrated form: a hand-written second copy of a platform check is not a check of the platform, it is a check of the copy. When the two disagree, nothing detects it. +## A probe reports observations parameterized by their capture; it does not draw the client's conclusion + + + +Every figure this crate publishes is **what one machine did on one day**, and it +is recorded together with the parameters of its capture -- at minimum the probe's +host banner (architecture, logical and core counts, SMT, cache groupings, +efficiency classes, NUMA nodes), the build profile, and the number of runs with +their dispersion. A ratio quoted without those is an anecdote, not data a reader +can compare against their own hardware. + +**Some capture parameters are approximated, and some are simply absent; the +difference is stated rather than elided.** Windows exposes no NUMA *distance* +table -- there is no Win32 equivalent of reading ACPI SLIT, as recorded at the +`proximity` site in +[core_affinity.rs](../windows-placement-probe/src/core_affinity.rs). What it does +expose, and what this crate uses as the best available analog, is the +**assignment of processors to NUMA nodes** -- which is what the banner's +`numa[...]` field carries, as processors-per-node. Device-to-node mapping is +obtainable on the same footing. That analog answers "are these two things in the +same domain", which is the question most placement decisions actually turn on; it +does not answer "how much further is node 2 than node 1", and no amount of +probing on Windows will. Memory configuration and BIOS state are not captured at +all. + +**Reading the analog is part of reading the figure.** A banner of `numa[16]` is +a *single-domain* machine, so measurements taken on it say nothing whatever about +cross-domain behaviour -- not "a little", nothing. A figure is only evidence +about the domain structure its banner records. + +**The conclusions this crate is willing to draw are coarse, mechanically +reasoned, and observation-backed** -- "the buffers should be in the same memory +domain as the executor" is the shape of a claim that earns its place, because it +follows from how the hardware works *and* the measurements agree. **Fine-grained +topological and layout choices are handed to the client, not made for them.** +Which position/reservation apportionment a queue should use is exactly such a +choice: the layout is a type parameter of the shipping queue, the probe measures +every candidate, and the note reports what it saw. It does not name a winner. + +This is why the apportionment claim in the queue-contention section was +*withdrawn in both directions* rather than reversed. The measurement stopped +supporting "re-apportioning is free", but it equally did not support "it costs +30%" -- one host, seven runs, against a control that wanders. The correct output +of a probe that cannot call something is a flag saying *measure this on your own +hardware*, never a verdict chosen because a verdict reads better. + +The failure this prevents is a reader inheriting a number as though it were a +property of the code. It is a property of the code **on that machine**, and the +distinction is the whole value of shipping the probe rather than only its output. + ## Three tiers, because "run all the probes" is not a safe instruction @@ -508,11 +558,17 @@ exactly the kind this crate's `doorbell_cost` notes warn about. **Those four figures predate a correction to the timing window and have not been retaken.** The qualitative finding is unaffected -- a debug build still swamps the effect -- but the numbers themselves were measured while the probe timed from this thread's clock rather than from the producers' own, which -overstated throughput at high producer counts. Measured on `x86_64 16p/8c` after the correction, with a -second run of the same build as the noise control: `reserving_mpsc` at sixteen producers moved from -35.0 to 49.8-52.8 ns/push, against a run-to-run spread of 2-6%. So the correction is worth roughly 45% -at the producer counts where the curve is the finding, and any figure in this note taken before it -should be read as optimistic until retaken on a known host. +overstated throughput at high producer counts. Measured on `x86_64 16p/8c` after the correction: +`reserving_mpsc` at sixteen producers moved from 35.0 to a median of 52.3 ns/push across seven runs +(46.3-55.6). The move is larger than that shape's own run-to-run spread on this host, so the direction +is not in doubt; the magnitude is a single host's observation. Any figure in this note taken before the +correction should be read as optimistic until retaken. + +**An earlier version of this paragraph put that run-to-run spread at "2-6%", which seven runs do not +support** -- the same shape and configuration ranges 18% at sixteen producers, and the layout rows below +range considerably wider. The 2-6% figure came from comparing two runs, which cannot measure a spread; it +is corrected here rather than quietly dropped because several conclusions in this note were written +against it, and one of them did not survive the correction (see the layout section below). **That is a constraint on HOW it runs, not an argument for keeping it out**, and an earlier draft of this paragraph confused the two -- it said the CI job "runs `cargo run` without `--release`", which is not true @@ -571,18 +627,24 @@ branch was measured as though it were the algorithm. | 16 | 0.88x | 2.37x | 1.00x | | 32 | 0.98x | 2.99x | 1.11x | -**Re-apportioning the bits is free.** 16/48 tracks 32/32 within noise in both -regimes, which is the expected result and worth stating as a confirmed -prediction rather than a discovery: both issue the same `lock cmpxchg` on the -same `u64`, so only the shift and mask constants differ. The 48-bit position -does force `head` and the per-slot `sequence` to 64 bits, and that cost does not -show up either. What this buys is the recurrence moving from 2^32 to 2^48 -- -from about 37 seconds of sustained maximum-rate pushing to about 28 days. +**Re-apportioning the bits looked free here, and that reading was withdrawn.** +The reasoning was that both layouts issue the same `lock cmpxchg` on the same +`u64`, so only the shift and mask constants differ, and the table above was read +as confirming it. The table cannot carry that weight: these are single-run +figures, and the same-code control measured later ranges 0.69-1.27x, which is +wider than most of the differences being called "noise" -- note that this very +table has 16/48 at 1.14x and 1.21x while the prose beneath it says "within +noise". See +[Re-measured on the shipping type](#d-queue-layout-observations) +below for the seven-run figures and the withdrawal. What the re-apportionment +buys is not in dispute: the recurrence moves from 2^32 to 2^48, from about 37 +seconds of sustained maximum-rate pushing to about 28 days. **Widening the word is not free, and how much it costs depends entirely on the regime.** Isolated, where the claim is the only thing happening, `cmpxchg16b` costs 2-3x and the penalty *grows* with contention. Drained, with a consumer -running, it is 5-12%. +running, it is 5-12%. This is the one conclusion in this section that the +seven-run re-measurement strengthened rather than withdrew. ### The drained regime flatters the slower layout, and the refusal counts say so @@ -646,46 +708,106 @@ worth least.** Outstanding reservations are bounded by how many producers are mid-flight -- hundreds, perhaps thousands -- and the field currently holds four billion. Giving up reservations nobody will allocate is what buys the position bits: 2^21 reservations leaves about a day, 2^12 leaves over a year, and 2^8 -leaves twenty years. The last is the same practical answer a 128-bit word gives, -on a plain `AtomicU64`, at no measured cost, without a third-party dependency and -without reopening `D-18`'s i686 question. +leaves twenty years. The last reaches the same practical headroom a 128-bit word +gives, on a plain `AtomicU64`, without a third-party dependency and without +reopening `D-18`'s i686 question. + +**This paragraph previously added "at no measured cost", and that clause is +withdrawn** -- it was the same claim the layout section below withdrew, restated +a third time in a section about counter arithmetic rather than about speed. The +arithmetic above is unaffected, because time-to-wrap follows from the field width +and a rate, not from a measurement of either layout; what does not follow is any +statement about what the re-apportionment costs to run. See +[Re-measured on the shipping type](#d-queue-layout-observations). So the candidates worth considering are **12/52 and 8/56**, not the 16/48 first sketched here: 16/48's 12.7 days at the conservative floor is still reachable by a busy long-lived process, and 12/52 is the first row that is not. -### Re-measured on the shipping type, and the duplicate had understated the wide word +### Re-measured on the shipping type, with the probe's own control to read it against + + `CW-1.6` deleted the duplicated protocol in this crate once `windows-waitable-queues` took the layout as a parameter, so the probe now instantiates the real type at each layout. The numbers below supersede the ones above, which were taken from the stand-in. +**Read every figure here as one host's observation, not as a portable result.** +The capture parameters are the probe's own banner, reproduced in full because a +ratio without them is an anecdote rather than data someone else can use: + +``` +host: x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16] +``` + +Seven runs, median of the per-run ratios with the observed range beside it, +release build. **The banner's `numa[16]` is load-bearing here: it means a single +NUMA node holding all sixteen processors**, so every figure below was taken +inside one memory domain and says nothing about cross-domain behaviour. What is +not pinned down at all is memory configuration and BIOS state; NUMA *distances* +are unavailable on Windows by platform limit rather than by omission, and the +processor-to-node assignment in the banner is the analog this crate uses in their +place (see [A probe reports observations parameterized by their +capture](#d-observations-not-verdicts)). + +The layout is a *parameter* of the shipping type, so this note's job is to report +what this machine did and hand the reader the tooling -- the probe -- to measure +the machine they actually care about. It is not to pick a winner on their behalf. + +**The probe emits its own noise control, and it is the only honest yardstick for +these ratios.** The `reserving_mpsc` row and the `reserving(32/32)` row are the +same code at the same layout, measured twice in the same run, so their ratio is +what "no difference" looks like on this host: + +| regime | same-code control (`reserving_mpsc` vs `32/32`) | +|---|---| +| isolated | median 0.94-1.05x, observed 0.69-1.12x | +| drained | median 0.98-1.07x, observed 0.68-1.27x | + +So a ratio inside roughly 0.9-1.1x is indistinguishable from zero effect here, +and at sixteen and thirty-two producers the control alone wanders past 1.12x. + | producers | 16/48 vs 32/32 | 8/56 vs 32/32 | 64/64 vs 32/32 | |---|---|---|---| -| 1 | 1.02x | 0.98x | 1.45x | -| 4 | 1.04x | 1.01x | 1.33x | -| 8 | 1.05x | 1.05x | 1.59x | -| 16 | 1.21x | 1.21x | **3.83x** | -| 32 | 1.05x | 1.13x | **3.99x** | - -**The finding about apportionment survives contact with the real type.** Both -`u64` re-apportionments track the default within noise, including `Perpetual`'s -8/56 -- so buying twenty years of headroom really is free, and it is now -measured on the code that ships rather than on something resembling it. - -**The finding about width did not survive unchanged.** The duplicate reported -the 128-bit exchange at 2.37x and 2.99x at sixteen and thirty-two producers; the -real type reports 3.83x and 3.99x. The stand-in was *understating* the cost of -the layout it was built to evaluate, and by the widest margin exactly where the -decision is most sensitive. The conclusion is unaltered in direction and firmer -in degree. +| 1 | 1.00x [0.74-1.00] | 1.00x [0.67-1.04] | 1.37x [1.16-1.57] | +| 2 | 0.94x [0.89-1.05] | 0.96x [0.80-0.98] | 1.13x [1.02-1.15] | +| 4 | 0.96x [0.83-1.03] | 1.00x [0.90-1.10] | 1.29x [1.14-1.36] | +| 8 | 1.01x [0.95-1.13] | 0.94x [0.92-1.08] | 1.82x [1.64-2.20] | +| 16 | 1.23x [1.09-1.35] | 1.26x [1.16-1.33] | **3.45x [2.91-4.27]** | +| 32 | 1.30x [1.15-1.41] | 1.28x [1.11-1.42] | **3.81x [2.70-4.31]** | + +In the drained regime nothing separates at all -- every u64 layout *and* the +128-bit word sit inside the control band at every producer count (the widest +median is 1.13x at one producer, against a control that reaches 1.27x). + +**Widening the word is the one effect this probe establishes.** At sixteen and +thirty-two producers the isolated 128-bit rows sit three to four times the +64-bit rows, an order of magnitude outside anything the same-code control does. +That is a real effect on this machine, and its direction is mechanically +unsurprising -- `cmpxchg16b` against `lock cmpxchg`. Whether it reproduces on +another microarchitecture is a question for the probe, not for this note. + +**The apportionment claim is withdrawn, in both directions.** This section +previously said the `u64` re-apportionments "track the default within noise" and +that twenty years of headroom is therefore "free". That was asserted from a +single run against a noise floor quoted as 2-6%, and neither half holds: the +measured control is far wider than 2-6%, and the re-apportionments do not sit +inside it at sixteen and thirty-two producers. But the replacement is *not* the +opposite claim. 1.23-1.30x against a control that itself reaches 1.12x is a +flag, not a finding -- it says this is the configuration worth measuring on your +own hardware before choosing, and it says this probe, on this host, at seven +runs, could not call it. A client who needs the headroom should measure the +layouts on their target rather than inherit either verdict from here. This is +[the rule for what this crate concludes](#d-observations-not-verdicts) applied to +the case that earned it. **The residual offset is gone, which is the point of the deletion.** The duplicate ran about 1.26x slower than `reserving_mpsc` at high producer counts, -an error that had to be carried as a caveat on every figure. Running the same -configuration twice through the shipping type now agrees within noise -- 50.3 ns -against 52.1 ns at thirty-two producers -- because both rows are the same code. +an error that had to be carried as a caveat on every figure. That the same-code +control now sits on 1.00x is what says the offset is gone -- and building that +control into the probe's output, rather than asserting a noise floor in prose, +is what let every ratio above be read honestly. The general lesson is worth keeping even though the duplicate is gone: **a stand-in is only evidence about the thing it stands in for while something @@ -693,6 +815,11 @@ checks that it still does.** This one was checked, which is how the missing cache padding was caught; but the checking only ever bounded the error, and the bound was loose enough to hide a third of the wide word's cost. +A second lesson the correction above earned: **a ratio means nothing without the +dispersion of the thing it is a ratio of.** Two runs cannot measure a spread, so +quoting one to two decimal places invites exactly the over-reading that produced +the withdrawn claim. Where this note gives a ratio it now gives the range too. + ## The report is buffered, and what that costs diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 2ae0566e9..37fa704a4 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -528,3 +528,59 @@ about THEM. The instruments remain exactly as good as the hand-sabotage that built them -- which is where several of this branch's defects were found, and where the next one will be. A clean sweep is evidence about the oracle, not about the things measuring it. + +## Why the crate reports observations instead of verdicts + +Recorded for [D-observations-not-verdicts](DESIGN-NOTES.md#d-observations-not-verdicts). + +The rule was earned, not designed. The queue-contention note had carried the +claim that re-apportioning a queue's position/reservation bits was **free** -- +that 16/48 and 8/56 "track the default within noise" -- and therefore that buying +twenty years of counter headroom cost nothing. Two independent defects sat under +that sentence. + +The first was arithmetic-shaped: the table directly beneath it showed 1.21x and +1.13x, against a noise floor the same document put at 2-6%. The prose +contradicted its own evidence, in adjacent lines, and survived several review +passes anyway -- because "within noise" reads as a conclusion rather than as a +claim about a measured quantity, so nobody checked it against the number. + +The second was deeper. The 2-6% floor had itself been obtained by comparing **two +runs**, which cannot measure a spread at all. Re-running the probe seven times +put the same-configuration spread at 7-61% depending on producer count. So the +floor every "within noise" judgement in the section had been made against was off +by roughly an order of magnitude, and the judgements were not recoverable by +adjusting it. + +What made the repair possible was already in the probe's output. `reserving_mpsc` +and `reserving(32/32)` are the same code at the same layout, measured twice per +run, so their ratio is an *empirical* answer to "what does no difference look +like here" -- 0.68-1.27x across seven runs. That is a control the instrument +derives rather than a floor the prose asserts, which is +[D-derived-not-restated](DESIGN-NOTES.md#d-derived-not-restated) applied to a +measurement instead of to a fact. + +**The tempting repair was to invert the claim**, since the seven-run medians put +the re-apportionments at 1.23-1.30x at high producer counts. That would have been +the same error with the opposite sign: one host, one microarchitecture, a single +NUMA domain, against a control whose own excursions reach 1.12x. The claim was +withdrawn in both directions instead, and the section now says which +configuration is worth measuring locally rather than what the answer is. + +This generalises to where the crate draws its line. Coarse claims that follow +from how the hardware works *and* are backed by observation -- "the buffers +should be in the same memory domain as the executor" -- are worth making, and +portable enough to be useful. Fine-grained topological and layout choices are +not: they depend on parameters the capture does not record and the reader's +machine does not share. The shipping queue takes its layout as a type parameter +precisely so that choice belongs to the client; a design note that quietly picks +one on their behalf takes it back. + +**On the capture parameters themselves.** Windows exposes no NUMA distance table, +so "how far apart are these nodes" is unanswerable on this platform. The analog +the crate uses is the processor-to-node assignment carried in the banner's +`numa[...]` field, with device-to-node mapping available on the same footing. It +answers the same-domain question, which is what most placement decisions turn on, +and it is why `numa[16]` on the measurement host is worth stating plainly: a +single domain means the queue figures say nothing about cross-domain behaviour at +all. From 2ffea52b7383f20235932b16ae4288a91286a6f1 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 18:52:35 -0400 Subject: [PATCH 005/139] docs(waitable-queues): drop "at no measured cost" from the layout guidance The withdrawn claim had propagated out of the probe's design note into this crate's consumer-facing documentation, where it told callers that `Perpetual` buys twenty years of headroom "at no measured cost". Three restatements plus the decision they derive from: - README.md, the "What to do about it" bullet - src/lib.rs, the same text as public rustdoc on docs.rs - README.md, the "Start here" chooser - DESIGN-NOTES.md D-41, "measured indistinguishable outside noise" The recurrence arithmetic is unaffected -- time-to-wrap follows from the field width and a rate, not from a throughput measurement -- so the recommendation stands. What is removed is the unsupported claim about what it costs to run. Re-measured, the deeper layouts are indistinguishable from `Balanced` up to eight producers and near 1.26x at sixteen and thirty-two, against a same-code control reaching 1.12x, on a single host. This is CONTRACT INTEGRITY rule 3: the reported site was a sample, not the population, and a correction to a shipped rule obliges a re-check of what was written against the old one. Verified: 13 doctests pass, including the compiled README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 14 ++++++++++---- crates/windows-waitable-queues/src/lib.rs | 12 +++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index b3360cd1e..80e61c738 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants, and measured indistinguishable outside noise -- so the recurrence moves from about 37 seconds to about 20 years for no throughput and no dependency. The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and it measured 2-3x slower on the claim; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` up to eight producers and sit near 1.26x at sixteen and thirty-two, against a same-code control reaching 1.12x -- which is one host declining to call it, not a cost. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and it measured 2-3x slower on the claim; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 0cfb87090..ec127c288 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -176,9 +176,14 @@ proportionally longer to reach its wrap. **What to do about it.** -- **Name a layout.** `Perpetual` puts the recurrence about twenty years out at - no measured cost, which takes it past any real deployment. This is the answer - for almost every caller who is exposed at all. +- **Name a layout.** `Perpetual` puts the recurrence about twenty years out, + which takes it past any real deployment. This is the answer for almost every + caller who is exposed at all. **What it costs in throughput is not + established** -- it issues the same `lock cmpxchg` on the same `u64` as the + default, and measured indistinguishable from it up to eight producers, but a + seven-run measurement on a single host put it near 1.26x at sixteen and + thirty-two producers against a same-code control that itself reached 1.12x. + Measure on your own target if throughput at high producer counts matters. - **`slotwise_mpsc` does not have this hazard** under any layout. Its positions are 64 bits on every target, so the equivalent wrap needs 2^64 claims. Prefer it unless you need `Reserving`. @@ -342,7 +347,8 @@ both rather than picking one for you. - **Pushing more than ~4 billion items in one run, from two or more producers?** Either use `slotwise_mpsc`, whose positions are 64 bits under every configuration, or name a deeper layout on `reserving_mpsc` -- `Perpetual` - puts the recurrence about twenty years out at no measured cost. Under its + puts the recurrence about twenty years out, though what it costs in throughput + is not established. Under its default layout `reserving_mpsc` can lose an item past that volume; see [the section on recurrence](#how-long-reserving_mpsc-runs-before-its-claim-position-recurs) above, which you should read before choosing. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 1c8817227..b57e18f06 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -146,9 +146,15 @@ //! //! **What to do about it.** //! -//! - **Name a layout.** `Perpetual` puts the recurrence about twenty years out -//! at no measured cost, which takes it past any real deployment. This is the -//! answer for almost every caller who is exposed at all. +//! - **Name a layout.** `Perpetual` puts the recurrence about twenty years out, +//! which takes it past any real deployment. This is the answer for almost +//! every caller who is exposed at all. **What it costs in throughput is not +//! established** -- it issues the same `lock cmpxchg` on the same `u64` as +//! the default, and measured indistinguishable from it up to eight producers, +//! but a seven-run measurement on a single host put it near 1.26x at sixteen +//! and thirty-two producers against a same-code control that itself reached +//! 1.12x. Measure on your own target if throughput at high producer counts +//! matters. //! - **[`slotwise_mpsc`] does not have this hazard** under any layout. Its //! positions are 64 bits on every target, so the equivalent wrap needs 2^64 //! claims. Prefer it unless you need [`Reserving`]. From 86e851b3c6b40e77cb95c947af399e2f26e797b6 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 19:11:22 -0400 Subject: [PATCH 006/139] docs(platform-probes): treat a wide self-control as a defect report, not a wider ruler The queue-contention repair derived a noise control from the probe's own output -- `reserving_mpsc` and `reserving(32/32)` are the same code at the same layout, measured twice per run -- and immediately used it to withdraw an unsupported claim. But it then used it only as a yardstick, which quietly promoted a symptom into a tool. Two measurements of identical code in the same run differing by 27%, with same-configuration spread reaching 61%, is first a finding about the METHOD. Recording it as merely a coarser ruler and carrying on is how a methodological problem becomes permanent. Records D-variance-is-a-finding. The candidate causes are not separable from the dispersion itself, so the note refuses to guess among them: wrong instrument for the variable, a residual probe defect (one was already found and fixed in d49a71f, invisible in the numbers and worth ~45%), too few runs or too short a span, or a nanosecond-scale measurement on a shared loaded desktop. The calibration is recorded rather than assumed, because it cuts both ways: a spread this wide would be disqualifying in a benchmark or marketing document, which exist to carry a comparative claim. These figures exist to support planning for deployment environments resembling the measured one -- an ordinary machine under ordinary load -- so the treatment is neither suppression nor promotion. Publish the data, publish the dispersion, and publish that the dispersion is unexplained. Queues M4.2 as a diagnosis item rather than leaving the work in a design note, per "design notes are not a work queue". It names a negative result as a valid outcome that must still be recorded, and forbids resolving it by suppressing the ranges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 38 ++++++++++++ .../windows-platform-probes/DESIGN-NOTES.md | 59 +++++++++++++++++++ .../DESIGN-RATIONALE.md | 44 ++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 68d9862ea..4c0e77f2e 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -67,6 +67,44 @@ correctness in the archive. own arithmetic does NOT belong, and the honest outcome for such a one is a line in the module header saying so by name rather than a silent absence. +- [ ] **M4.2** -- Find out why the probe's own same-code control varies by tens of percent, and + record the answer whatever it turns out to be. + + **Gap:** `queue_contention` emits `reserving_mpsc` and `reserving(32/32)`, which are the same code + at the same layout measured twice in one run. Their ratio should be 1.00x. Measured across seven + runs it spans 0.68-1.27x, and the same-configuration spread on a single shape reaches 61%. That + control is currently load-bearing -- the layout conclusions in + [DESIGN-NOTES.md](DESIGN-NOTES.md) are read against it, and it is wide enough that the + apportionment question could not be called either way. A control that wanders this far is evidence + about the instrument before it is evidence about the queue. + + **This is a diagnosis item, not a fix item.** The dispersion alone cannot distinguish the + candidates, so the deliverable is *which one it is*, with the measurement that shows it: + + - the probe moving more than the variable under test (wrong instrument for the question); + - a residual defect in the probe, as with the timing window corrected in `d49a71f`, which was + invisible in the numbers and worth roughly 45% at high producer counts; + - insufficient runs or too short a measured span -- plain hygiene, and the cheapest to rule out, + so rule it out first by raising both and seeing whether the control narrows; + - machine noise, these being nanosecond-scale measurements on a shared desktop under ordinary + load. Testable by re-running pinned, on a quiesced machine, or both. + + **A negative result is a real result here and must be recorded, not discarded.** If the answer is + "this is what a shared desktop does at this timescale and the probe is sound", that belongs in + [DESIGN-NOTES.md](DESIGN-NOTES.md) beside the figures, because it tells every future reader how + much weight the numbers carry. The failure mode to avoid is quietly widening the band again and + moving on. + + **Do not resolve this by suppressing the dispersion.** Reporting a median without its range, or + discarding outlying runs, would hide the open question rather than answer it. Whatever is found, + the ranges stay published. + + Decision: [High variance in our own control is a finding about the + instrument](DESIGN-NOTES.md#d-variance-is-a-finding). Note that per that decision the *calibration* + is already settled and is not part of this item: a spread this wide would disqualify a benchmarking + claim, while remaining a usable input for planning on comparable hardware. This item asks why it is + there, not whether the data may be published. + - [ ] **M2.5** -- Make the banner describe the read the body describes. Gated by M3.1 and M3.3, both landed: establishing that the middle of three discoveries agreed diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index fe8cfe93f..443c9101d 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -69,6 +69,47 @@ The failure this prevents is a reader inheriting a number as though it were a property of the code. It is a property of the code **on that machine**, and the distinction is the whole value of shipping the probe rather than only its output. +### High variance in our own control is a finding about the instrument, not just a wider yardstick + + + +When the same code measured twice in the same run disagrees by tens of percent, +the first thing that has been measured is **the method**. It is tempting to treat +a wide control as merely a coarser ruler -- to widen the band and carry on +judging ratios against it -- and that is the mistake this decision exists to stop. +A control that wanders is a defect report against the measurement, and it is +logged as one even when the measurement is still used. + +The candidate causes are not distinguishable from the dispersion alone, and all +of them are live here: + +- **The wrong instrument for the variable.** A probe that moves several things at + once cannot attribute a difference to the one under test. +- **A defect in the probe itself.** This crate has already shipped one -- the + timing window that read the coordinator's clock rather than the producers'. + That defect was invisible in the numbers until it was found by reading, and it + moved high-producer figures by roughly 45%. +- **Insufficient runs or too short a duration** -- straightforward hygiene, and + the cheapest to rule out. +- **A noisy machine.** These are fine-grained measurements taken on a shared, + general-purpose desktop running everything else it normally runs. Scheduling, + frequency scaling, and other tenants all land inside the timed region. + +**How much this matters depends entirely on what the number is for, and that +calibration is recorded rather than assumed.** In benchmarking or marketing +literature it would be disqualifying: those documents exist to support a +comparative claim, and a comparative claim resting on a control this wide is not +supported. Here the purpose is *planning for deployment environments resembling +the measured one* -- and a figure gathered on an ordinary loaded machine is not +obviously the wrong input for planning on ordinary loaded machines. So the +honest treatment is neither to suppress the data nor to promote it: **record it, +record the dispersion beside it, and record that the dispersion is itself +unexplained.** + +What this decision forbids is the quiet version -- reporting a wide control as +though a wide control were normal. It is not normal. It is an open question, and +where it is open, the note says so and the checklist carries the work. + ## Three tiers, because "run all the probes" is not a safe instruction @@ -768,6 +809,24 @@ what "no difference" looks like on this host: So a ratio inside roughly 0.9-1.1x is indistinguishable from zero effect here, and at sixteen and thirty-two producers the control alone wanders past 1.12x. +**That control is far too wide, and saying so is part of reporting it.** Two +measurements of *the same code in the same run* should not differ by 27%, and +the same-configuration spread across seven runs reaches 61%. Used above as a +yardstick, this is the honest yardstick available -- but a yardstick this elastic +is first a defect report against the probe, not a fact about the queue. The cause +is not determined: it could be the probe measuring more than the variable under +test, a residual defect like the timing window already found and fixed here, too +few runs or too short a measured span, or simply that these are nanosecond-scale +measurements taken on a shared desktop that is doing other things. The dispersion +alone cannot distinguish them, and this note does not guess. See +[High variance in our own control is a finding about the +instrument](#d-variance-is-a-finding), and M4.2 in +[CHECKLIST.md](CHECKLIST.md) for the work. + +What follows is therefore reported as *data with a known-unexplained spread*, +which is a reasonable input for planning a deployment on comparable hardware and +an unreasonable basis for a comparative claim about the layouts. + | producers | 16/48 vs 32/32 | 8/56 vs 32/32 | 64/64 vs 32/32 | |---|---|---|---| | 1 | 1.00x [0.74-1.00] | 1.00x [0.67-1.04] | 1.37x [1.16-1.57] | diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 37fa704a4..b5fb69965 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -584,3 +584,47 @@ answers the same-domain question, which is what most placement decisions turn on and it is why `numa[16]` on the measurement host is worth stating plainly: a single domain means the queue figures say nothing about cross-domain behaviour at all. + +### Why a wide control is logged as a defect rather than absorbed + +Recorded for [D-variance-is-a-finding](DESIGN-NOTES.md#d-variance-is-a-finding). + +The queue-contention repair produced a genuinely useful artifact -- a noise +control the probe derives rather than asserts, built from two rows that are the +same code at the same layout. It immediately did its job, withdrawing a claim +that had survived several reviews. + +It also very nearly produced a second error. Having found that the control spans +0.68-1.27x, the natural next move is to use it: judge every ratio against that +band, mark what falls outside, and report the result. That is what the first +draft of the section did. But two measurements of identical code in the same run +differing by 27% is not a fact about the queue at all -- it is the instrument +telling you something, and using it as a ruler while declining to ask why it is +elastic is how a methodological problem becomes permanent. The control had been +promoted from *symptom* to *tool* without anyone deciding to do that. + +The causes are not separable from the dispersion itself, which is precisely why +the decision refuses to guess among them. The list is short and every entry is +plausible here: the probe may be moving more than the variable under test; it may +carry a residual defect, as it demonstrably did until the timing window was +corrected; seven runs of a ~65-second probe may simply be too few; or a +nanosecond-scale measurement on a shared desktop running everything else may be +dominated by the machine. Only the third is cheap to rule out, which is why the +checklist item says to try it first. + +**The calibration is the part worth writing down, because it is not obvious and +it cuts both ways.** A spread like this in a benchmark or a marketing document +would be fatal -- such documents exist to carry a comparative claim, and a +comparative claim resting on a control this wide is simply unsupported. But this +crate's figures exist to support *planning for deployment environments like the +measured one*, and the measured one is an ordinary machine under ordinary load. +Data gathered there is not obviously the wrong input for planning there. So the +answer is neither suppression nor promotion: publish it, publish the dispersion, +and publish the fact that the dispersion is unexplained. A reader can then weigh +it for their own purpose, which is the same principle as +[D-observations-not-verdicts](DESIGN-NOTES.md#d-observations-not-verdicts) applied +to the quality of the measurement instead of to its portability. + +The rule exists to forbid the quiet version: reporting a wide control as though a +wide control were ordinary. It is not ordinary, and the moment it stops being +remarked upon is the moment nobody investigates it. From a9d74277fd2b440831da12837316e75b54fd9e21 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 19:27:19 -0400 Subject: [PATCH 007/139] docs(platform-probes): say what to try first on high variance, and where the floor is Extends D-variance-is-a-finding with the practical half, and re-scopes M4.2 from "diagnose the variance" to "give the probes the controls that make diagnosing it possible" -- the judgement stays with the person, the probe stops being the obstacle. What the note now says: - Gather more of the same before gathering anything different. Lengthening the span and raising the repetition count is the only step that changes nothing about what is measured, so it is the only one whose result is interpretable before the others have been tried. Pinning, quiescing, or altering the probe all move the measurement as well as the noise. - A warmup separates transient cost from ongoing noise. Recorded with the objection it invites: if noise were inherent, warming could not remove it. It does not -- warming removes front-loaded transients (page faults, cold caches, frequency ramp) while tenant contention runs for the whole measurement. So warming uncovers the inherent floor rather than hiding it, and the expected signature is a spread that narrows and then plateaus. - There is always a floor, and recognising it is the skill. Past it more runs buy nothing. - The floor is NOT necessarily a fraction of the measured value. It can be set by the sampling regime -- clock granularity, independent sample count, how the span is built. This probe takes two timestamps per worker per repetition, so what limits resolution at small values is the pass count, not a percentage of the nanoseconds printed. The two readings prescribe opposite actions, which is why "small numbers are just noisy" is a guess and not a diagnosis. M4.2's software scope, verified against the source: PUSHES_PER_PRODUCER (50_000) and REPETITIONS (5) are private consts and measure() takes no arguments, so the prescribed first move currently requires a source edit and a rebuild. Defaults must not change, and whatever settings a run used have to be reported beside the host banner -- they are capture parameters now, per D-observations-not-verdicts. Note the warmup is partly present already: one untimed pass exists, but only to fault in the allocation's pages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 74 +++++++++---------- .../windows-platform-probes/DESIGN-NOTES.md | 49 +++++++++++- .../DESIGN-RATIONALE.md | 39 +++++++++- 3 files changed, 121 insertions(+), 41 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 4c0e77f2e..70b2cfbfd 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -67,43 +67,43 @@ correctness in the archive. own arithmetic does NOT belong, and the honest outcome for such a one is a line in the module header saying so by name rather than a silent absence. -- [ ] **M4.2** -- Find out why the probe's own same-code control varies by tens of percent, and - record the answer whatever it turns out to be. - - **Gap:** `queue_contention` emits `reserving_mpsc` and `reserving(32/32)`, which are the same code - at the same layout measured twice in one run. Their ratio should be 1.00x. Measured across seven - runs it spans 0.68-1.27x, and the same-configuration spread on a single shape reaches 61%. That - control is currently load-bearing -- the layout conclusions in - [DESIGN-NOTES.md](DESIGN-NOTES.md) are read against it, and it is wide enough that the - apportionment question could not be called either way. A control that wanders this far is evidence - about the instrument before it is evidence about the queue. - - **This is a diagnosis item, not a fix item.** The dispersion alone cannot distinguish the - candidates, so the deliverable is *which one it is*, with the measurement that shows it: - - - the probe moving more than the variable under test (wrong instrument for the question); - - a residual defect in the probe, as with the timing window corrected in `d49a71f`, which was - invisible in the numbers and worth roughly 45% at high producer counts; - - insufficient runs or too short a measured span -- plain hygiene, and the cheapest to rule out, - so rule it out first by raising both and seeing whether the control narrows; - - machine noise, these being nanosecond-scale measurements on a shared desktop under ordinary - load. Testable by re-running pinned, on a quiesced machine, or both. - - **A negative result is a real result here and must be recorded, not discarded.** If the answer is - "this is what a shared desktop does at this timescale and the probe is sound", that belongs in - [DESIGN-NOTES.md](DESIGN-NOTES.md) beside the figures, because it tells every future reader how - much weight the numbers carry. The failure mode to avoid is quietly widening the band again and - moving on. - - **Do not resolve this by suppressing the dispersion.** Reporting a median without its range, or - discarding outlying runs, would hide the open question rather than answer it. Whatever is found, - the ranges stay published. - - Decision: [High variance in our own control is a finding about the - instrument](DESIGN-NOTES.md#d-variance-is-a-finding). Note that per that decision the *calibration* - is already settled and is not part of this item: a spread this wide would disqualify a benchmarking - claim, while remaining a usable input for planning on comparable hardware. This item asks why it is - there, not whether the data may be published. +- [ ] **M4.2** -- Give the measurement probes the controls needed to act on a dispersion finding, + so "gather more data along this axis" does not require editing a `const` and rebuilding. + + **This item is deliberately small in software and large in guidance.** The diagnostic method + belongs in [DESIGN-NOTES.md](DESIGN-NOTES.md) -- see + [What to try first, and how to tell when you have reached the + floor](DESIGN-NOTES.md#d-variance-is-a-finding) -- and this item exists only to make that method + executable. The judgement stays with the person; the probe stops being the obstacle. + + **Gap:** [src/queue_contention.rs](src/queue_contention.rs) fixes every sampling parameter as a + private constant -- `PUSHES_PER_PRODUCER` (50,000) and `REPETITIONS` (5) -- and `measure()` takes + no arguments. The first move the design note prescribes on seeing a wide control is to lengthen + the span and raise the repetition count on the unchanged configuration, which is currently a + source edit and a rebuild. A control that cannot be turned is not a control, and the cheapest + diagnostic step is the one being blocked. + + **Target:** `measure()` takes a settings value carrying at least the pushes-per-producer count, + the repetition count, and the producer counts to sweep (`PRODUCER_COUNTS` is already public and + is the model for the others). Existing defaults stay exactly as they are, so a default run remains + the run the notes describe and every published figure stays reproducible. The binary exposes the + same knobs so a human or an agent can act without a rebuild. + + Apply the same treatment to the sibling cost probes where the sampling parameters are equally + fixed; the axes we anticipate varying are **duration, repetitions, and concurrency**, so those are + the ones that need to be reachable. Do not add knobs beyond what a stated diagnostic step needs -- + an unused parameter is a configuration surface to maintain and a way for two runs to differ + without anyone noticing. + + **Report what was used.** Whatever settings a run was given must appear in its output beside the + host banner, for the reason + [D-observations-not-verdicts](DESIGN-NOTES.md#d-observations-not-verdicts) already gives: a figure + is only interpretable with its capture parameters, and these are now among them. Making the + sampling adjustable without recording it would turn one reproducibility problem into a worse one. + + **Not in scope:** deciding why the control is wide. That is the judgement this tooling supports, + and per the design note a negative result -- "lengthening and repeating do not narrow it, so the + floor is here" -- is a real answer that gets recorded beside the figures. - [ ] **M2.5** -- Make the banner describe the read the body describes. diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 443c9101d..0f13a3b30 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -106,6 +106,50 @@ honest treatment is neither to suppress the data nor to promote it: **record it, record the dispersion beside it, and record that the dispersion is itself unexplained.** +### What to try first, and how to tell when you have reached the floor + +**The cheapest move is always to gather more of the same before gathering +anything different.** Lengthen the timed span, raise the repetition count, or +both, on the *unchanged* configuration. This costs only wall time and it +partitions the problem in one step: if the control narrows, the dispersion was +sampling noise and the previous run simply had too few samples to resolve +anything; if it does not, the width is structural and the remaining candidates +are the interesting ones. Do this before pinning threads, before quiescing the +machine, and before suspecting the probe -- each of those changes what is being +measured, and a change made before the cheap check cannot be evaluated. + +**A warmup pass separates transient cost from ongoing noise, and the two are +different things.** This probe already discards one untimed pass, but only to +fault in a fresh allocation's pages. Cold caches, branch predictors, and CPU +frequency ramp are the same *kind* of cost -- one-time, front-loaded, not a +property of the steady state -- and lengthening the timed span dilutes them +whether or not a warmup removes them. + +It is worth being clear that this does **not** contradict the position that some +noise is inherent to a shared machine. A warmup removes *transients*; contention +with other tenants continues for the whole run and is not removable by any amount +of warming. The two widen dispersion for unrelated reasons, and removing the +transients is what makes the inherent floor *visible* rather than what hides it. +Expect warming and lengthening to shrink the spread to some value and then stop +shrinking it, and treat that plateau as the interesting result. + +**There is always a floor, and recognising it is the skill this decision is +really about.** A measurement cannot resolve a difference smaller than the noise +in the quantity being differenced, and past that point more runs buy nothing -- +continuing to gather them is how a project spends a week proving that two numbers +are the same. The floor is a real, findable property of the setup, not a failure. + +**The floor is not necessarily a percentage of the measured value**, and assuming +it is will mislead you in both directions. It can be set by the sampling regime +instead: the granularity of the clock, how many independent samples the run +actually takes, or how the measured span is constructed. This probe times a whole +pass and divides -- two timestamps per worker per repetition -- so at small +absolute values the resolvable difference is governed by how many independent +passes were taken, not by any fixed fraction of the nanoseconds reported. That is +why "the 1-producer rows are noisy because the numbers are small" is a guess +rather than a diagnosis, and why the first move above is to add samples: it tests +that guess directly. + What this decision forbids is the quiet version -- reporting a wide control as though a wide control were normal. It is not normal. It is an open question, and where it is open, the note says so and the checklist carries the work. @@ -820,8 +864,9 @@ few runs or too short a measured span, or simply that these are nanosecond-scale measurements taken on a shared desktop that is doing other things. The dispersion alone cannot distinguish them, and this note does not guess. See [High variance in our own control is a finding about the -instrument](#d-variance-is-a-finding), and M4.2 in -[CHECKLIST.md](CHECKLIST.md) for the work. +instrument](#d-variance-is-a-finding) for what to try first and how to recognise +the floor, and M4.2 in [CHECKLIST.md](CHECKLIST.md) for the probe controls that +make those steps executable without a source edit. What follows is therefore reported as *data with a known-unexplained spread*, which is a reasonable input for planning a deployment on comparable hardware and diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index b5fb69965..81948048f 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -609,8 +609,43 @@ plausible here: the probe may be moving more than the variable under test; it ma carry a residual defect, as it demonstrably did until the timing window was corrected; seven runs of a ~65-second probe may simply be too few; or a nanosecond-scale measurement on a shared desktop running everything else may be -dominated by the machine. Only the third is cheap to rule out, which is why the -checklist item says to try it first. +dominated by the machine. + +**The ordering of the diagnosis is the practical content, and it follows from +cost rather than from likelihood.** Lengthening the span and raising the +repetition count is the only step that changes nothing about what is being +measured -- so it is the only step whose result is interpretable before the +others have been tried. Pinning threads, quiescing the machine, or altering the +probe all move the measurement as well as the noise, and a change made ahead of +the cheap check cannot be evaluated against anything. That this also happens to +be the least effortful step is a convenience, not the reason. + +The other half is knowing when to stop. Every setup has a floor, and past it more +runs buy nothing; the failure mode is a week spent establishing that two numbers +are the same. What makes the floor easy to misjudge is the assumption that it +scales with the measured value -- that small numbers are inherently noisy. It can +just as well be set by the sampling regime: clock granularity, how many +independent samples the run takes, how the span is constructed. This probe takes +two timestamps per worker per repetition and divides by the pass, so what limits +resolution at small values is the number of passes, not a fraction of the +nanoseconds printed. The distinction matters because the two readings prescribe +opposite actions -- one says the measurement is hopeless, the other says take +more samples. + +**A warmup pass looks like it contradicts the "some noise is inherent" position, +and the objection is worth answering rather than smoothing over.** If dispersion +were genuinely inherent to a shared machine, warming could not remove it -- so +proposing a warmup appears to concede that it is really an artifact after all. +The resolution is that the two are different quantities that happen to widen the +same spread. A warmup addresses *transients*: page faults on a fresh allocation, +cold caches and predictors, frequency ramp -- all front-loaded, all one-time, none +a property of the steady state. Contention with other tenants is not front-loaded; +it continues for the whole run and no amount of warming touches it. Removing the +transients therefore does not hide the inherent floor, it uncovers it, which is +why the expected signature is a spread that narrows as warming and length +increase and then stops narrowing. The plateau is the inherent part. This probe +already discards one untimed pass, though only for the allocation's pages, so +part of this is done and the rest is unmeasured. **The calibration is the part worth writing down, because it is not obvious and it cuts both ways.** A spread like this in a benchmark or a marketing document From 123bdbe9ac5b9cbde909dd4552db2c153e79eb82 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 19:31:10 -0400 Subject: [PATCH 008/139] docs(platform-probes): record the sampling parameters as capture parameters This PR added D-observations-not-verdicts, which requires every published figure to carry the parameters of its capture, and then published figures missing two of them. The block recorded the host banner, the build profile, and "seven runs" -- but not what a run consists of. Each run is a whole probe invocation, within which every configuration is measured REPETITIONS (5) times and the median reported, each measurement being PUSHES_PER_PRODUCER (50,000) pushes per producer thread, preceded by one untimed pass that faults in the fresh allocation's pages. So each published figure rests on 35 timed passes per configuration, and "seven runs" alone does not let anyone reproduce it. Also names where the two constants live and points at M4.2, which makes them adjustable -- the note now prescribes lengthening the span and raising the repetition count as the first response to a wide control, and the probe cannot currently do either without a source edit. Recording the gap is what this PR owes; closing it is follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/DESIGN-NOTES.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 0f13a3b30..21c025849 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -827,7 +827,18 @@ host: x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16] ``` Seven runs, median of the per-run ratios with the observed range beside it, -release build. **The banner's `numa[16]` is load-bearing here: it means a single +release build. **The sampling parameters are capture parameters too**: each run +is a whole probe invocation, within which every configuration is measured five +times and the median reported, each measurement being 50,000 pushes per producer +thread, preceded by one untimed pass that exists to fault in the fresh +allocation's pages. So a figure below rests on 35 timed passes per +configuration, and "seven runs" alone would not let anyone reproduce it. These +are fixed at +[src/queue_contention.rs](src/queue_contention.rs)`::PUSHES_PER_PRODUCER` and +`REPETITIONS`; M4.2 in [CHECKLIST.md](CHECKLIST.md) makes them adjustable, which +is what the first diagnostic step above needs and cannot currently do. + +**The banner's `numa[16]` is load-bearing here: it means a single NUMA node holding all sixteen processors**, so every figure below was taken inside one memory domain and says nothing about cross-domain behaviour. What is not pinned down at all is memory configuration and BIOS state; NUMA *distances* From a4b752af4472d5002bb937e95529983a037f9ca0 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 20:02:09 -0400 Subject: [PATCH 009/139] docs(platform-probes): withdraw the drained widening figure too The re-measured section says every drained layout -- including the 128-bit word -- sits inside the same-code control band, but the section above it still told the reader the drained "5-12%" was "the one conclusion the seven-run re-measurement strengthened rather than withdrew". Both were written in this PR, three paragraphs apart, and they cannot both be true. The isolated half is what was strengthened (2-3x, re-measured to 3.45x/3.81x at 16/32 producers). The drained half is withdrawn, and the reason is worth stating precisely: the re-measured drained rows run 2-13%, which RESEMBLES the old 5-12% closely enough to look like confirmation, while every one of them sits inside a control spanning -32% to +27%. A number that agrees with its predecessor is not thereby established -- that is exactly the trap a same-code control exists to catch, and this is the case where it caught it. Also fixes the section heading, which asserted the withdrawn claim outright ("...and much less in use"). Found by review. My earlier sweep for this claim matched on PHRASINGS I had used rather than on the proposition, which is why it missed this. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 21c025849..da314a0df 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -683,7 +683,7 @@ count from the queue's own `Observable` counters precisely so that is visible as mistaken for contention: the sixteen- and thirty-two-producer drained rows show millions of refusals and should be read as measurements of the consumer. -## The claim word's width costs 2-3x in isolation and much less in use +## The claim word's width costs 2-3x in isolation, and the drained figure is withdrawn Measured by `probe-queue-contention` on one host, `x86_64-pc-windows-msvc`. Three apportionments of `reserving_mpsc`'s claim word: 32/32 and 16/48 over @@ -725,11 +725,18 @@ below for the seven-run figures and the withdrawal. What the re-apportionment buys is not in dispute: the recurrence moves from 2^32 to 2^48, from about 37 seconds of sustained maximum-rate pushing to about 28 days. -**Widening the word is not free, and how much it costs depends entirely on the -regime.** Isolated, where the claim is the only thing happening, `cmpxchg16b` -costs 2-3x and the penalty *grows* with contention. Drained, with a consumer -running, it is 5-12%. This is the one conclusion in this section that the -seven-run re-measurement strengthened rather than withdrew. +**Widening the word is not free in the isolated regime, and the drained figure +below does not survive the re-measurement.** Isolated, where the claim is the +only thing happening, `cmpxchg16b` costs 2-3x and the penalty *grows* with +contention; that is the one conclusion in this section the seven-run +re-measurement strengthened, to 3.45x and 3.81x at sixteen and thirty-two +producers. The drained figure of 5-12% is **withdrawn** -- not because the +number moved, but because nothing was measuring whether it meant anything. The +re-measured drained 128-bit rows run 2-13%, which resembles the old figure +closely enough to look like confirmation, while every one of them sits inside a +same-code control spanning -32% to +27%. A number that agrees with its +predecessor is not thereby established; that is precisely the trap the control +exists to catch, and this is the case where it catches it. ### The drained regime flatters the slower layout, and the refusal counts say so From 0759b5cb69324b52fcc96a775b97d46534348678 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 20:02:09 -0400 Subject: [PATCH 010/139] docs(waitable-queues): three more sites carrying the withdrawn cost claim The earlier sweep matched on the phrasings I had written ("no measured cost", "indistinguishable outside noise") instead of on the claim itself, so it missed three sites that state the same withdrawn proposition in different words: - README.md: "A deeper position costs nothing measurable ... found no difference outside noise" - src/lib.rs: the same text as public rustdoc - src/reserving_mpsc.rs: "Choosing a deeper position costs nothing measurable" on ClaimLayout itself, plus "nothing measurable besides" in the module header ClaimLayout's own rustdoc is the most consumer-facing of the three -- it is what a caller reads while choosing a layout. The mechanical argument is kept because it is sound and independent of any measurement: all three layouts issue the same lock cmpxchg on the same u64 and differ only in shift and mask constants, so there is no structural reason for one to be slower. What is removed is the claim that this was MEASURED to cost nothing. Replaced with the seven-run result and its control, and a pointer to measure on the caller's own target. Verified: 13 doctests pass including the compiled README; 304 lib tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/README.md | 15 +++++++++------ crates/windows-waitable-queues/src/lib.rs | 15 ++++++++++----- .../src/reserving_mpsc.rs | 18 ++++++++++++------ 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index ec127c288..b547e693f 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -139,12 +139,15 @@ let (tx, rx) = reserving_mpsc::bounded_as::(64)?; # Ok::<(), windows_waitable_queues::CapacityError>(()) ``` -**A deeper position costs nothing measurable.** `Balanced`, `Enduring`, and -`Perpetual` all issue the same exchange on the same 64-bit word and differ only -in shift and mask constants; a probe comparing them found no difference outside -noise. `Wide` is the exception: it needs a 128-bit exchange, which measured 2-3x -slower on the claim, and it is the only thing in this crate that costs a -third-party dependency. +**A deeper position is the same exchange on the same word.** `Balanced`, +`Enduring`, and `Perpetual` all issue the same exchange on the same 64-bit word +and differ only in shift and mask constants, so there is no structural reason for +one to be slower -- but **what that costs in throughput is not established**: a +probe comparing them found them indistinguishable up to eight producers and near +1.26x at sixteen and thirty-two, on one host, against a same-code control that +itself reached 1.12x. `Wide` is a separate matter: it needs a 128-bit exchange, +which measured 2-3x slower on the claim in isolation, and it is the only thing in +this crate that costs a third-party dependency. The default remains `Balanced` so that no existing caller's behaviour changed when the choice was introduced. It is not the recommended layout. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index b57e18f06..b5bf5218b 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -107,11 +107,16 @@ //! # Ok::<(), windows_waitable_queues::CapacityError>(()) //! ``` //! -//! **A deeper position costs nothing measurable.** `Balanced`, `Enduring`, and -//! `Perpetual` all issue the same exchange on the same 64-bit word and differ -//! only in shift and mask constants; a probe comparing them found no difference -//! outside noise. `Wide` is the exception: it needs a 128-bit exchange, which -//! measured 2-3x slower on the claim, and it is the only thing in this crate +//! **A deeper position is the same exchange on the same word.** `Balanced`, +//! `Enduring`, and `Perpetual` all issue the same exchange on the same 64-bit +//! word and differ only in shift and mask constants, so there is no structural +//! reason for one to be slower -- but **what that costs in throughput is not +//! established**: a probe comparing them found them indistinguishable up to +//! eight producers and near 1.26x at sixteen and thirty-two, on one host, +//! against a same-code control that itself reached 1.12x. `Wide` is a separate +//! matter: it needs a 128-bit exchange, which +//! measured 2-3x slower on the claim in isolation, and it is the only thing in +//! this crate //! that costs a third-party dependency. Prefer `Perpetual` unless you want the //! guarantee rather than the twenty years. //! diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index e1cfa4dfc..5bcbd3398 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -27,8 +27,9 @@ //! //! **[`ClaimLayout`] is how far away that is.** [`Perpetual`] moves it to 2^56 //! pushes, about twenty years at the same rate, for the cost of a reservation -//! ceiling of 255 and nothing measurable besides -- it is the same exchange on -//! the same word, differing only in shift constants. [`Enduring`] sits between +//! ceiling of 255 -- it is the same exchange on +//! the same word, differing only in shift constants, though what that costs in +//! throughput is not established (see [`ClaimLayout`]). [`Enduring`] sits between //! them, and the `dwcas` feature adds a 128-bit word that removes the //! recurrence outright. //! @@ -183,10 +184,15 @@ use crate::options::Options; /// note quotes; a queue that must drain cannot sustain the fastest rate /// measured, so treat these as a floor on time rather than a forecast. /// -/// **Choosing a deeper position costs nothing measurable.** All three issue the -/// same `lock cmpxchg` on the same `u64` and differ only in shift and mask -/// constants; a probe comparing them found no difference outside noise. The -/// trade is entirely against the reservation ceiling. +/// **Choosing a deeper position is the same instruction on the same word.** All +/// three issue the same `lock cmpxchg` on the same `u64` and differ only in +/// shift and mask constants, so there is no structural reason for one to be +/// slower. **What that costs in throughput is not established**: a probe +/// comparing them found them indistinguishable up to eight producers and near +/// 1.26x at sixteen and thirty-two, on one host, against a same-code control +/// that itself reached 1.12x. Measure on your target if throughput at high +/// producer counts matters. The trade is otherwise entirely against the +/// reservation ceiling. /// /// This trait is sealed: the layouts are a fixed set because each one's /// constants are checked against each other at compile time, and a caller From 2d29bee27daffc055e592794b521f11f10b016ea Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 19:19:06 -0700 Subject: [PATCH 011/139] fix(platform-probes): restore the i686 build this probe's features removed `windows-platform-probes` enabled `dwcas` on `windows-waitable-queues` unconditionally. Feature unification propagates that to every build of the workspace, so `i686-pc-windows-msvc` stopped compiling: error[E0433]: cannot find `AtomicU128` in `portable_atomic` --> crates/windows-waitable-queues/src/reserving_mpsc.rs:434:26 That is not a tuning detail, it is the queue crate's contract. D-18 records that i686 has no 128-bit atomic and that adopting one is "widen the word AND drop 32-bit support"; D-40 says the workspace deliberately keeps i686 supported; D-37 designed `dwcas` as non-default precisely so the narrow word ships everywhere and the wide one only where it is genuinely lock-free. Forcing the feature from a probe took that choice away from every other crate in the workspace -- PLATFORM INTEGRITY rule 3, narrowing the platform to serve the visible goal. Nothing detected it because CI builds no cross-target job. Fixed by moving `dwcas` to a target-specific dependency naming the two architectures the queue crate documents as natively lock-free (x86-64 via `cmpxchg16b`, aarch64 via `ldxp`/`stxp`), and gating the `Wide` import and its two `measure()` push sites on the same condition. `target_has_atomic = "128"` would be the wrong test -- D-37 records that rustc emits it even with `cmpxchg16b` disabled. The renderer already degrades a missing row to `--`, so a target without the wide exchange reports the other three layouts and leaves that column empty. Verified both directions: `cargo check -p windows-platform-probes --target i686-pc-windows-msvc` now succeeds where it previously failed, and a release run on x86-64 still emits all 12 `reserving(64/64)` rows. Also in this commit, all from the same review: - The renderer printed the claim this branch withdrew -- "a difference between them is noise or slot-metadata density, not the claim". That is the probe's own output, the most-read surface it has. It now states the mechanical argument (same instruction, shift constants only) without the measured conclusion, and points at the same-code control. - `cmpxchg16b` was named as though it were the only instruction; now target-aware. - "the price of reservation" over-attributed an end-to-end two-shape difference to `reserving_mpsc`'s extra `head` load. Retitled, and the ratio is now described as bounding that load's contribution from above rather than isolating it. - The report omitted its sampling parameters, which D-observations-not-verdicts requires. It now prints pushes-per-producer, repetition count, and the warmup pass; the two constants became `pub` so the binary can read them. - `queue_contention` had no row in the crate's "What each probe establishes" index, and no sibling tests despite `find`/`scaling` deciding whether the report shows a ratio or `--`. Added both: 16 pure tests, 0.00s, no benchmark needed. - The drained consumer comment implied the barrier closes the undrained window. It guarantees arrival, not that the consumer pops before the first push. Comment corrected to say what the mechanism delivers; closing the window needs a readiness handshake, queued as M4.3 with its blocker named (it changes the measurement and obsoletes every published figure). - M4.2 updated: the sampling constants are no longer private, and it now also covers `median_run` discarding four of five samples, which is why the published dispersion had to be computed by hand outside the probe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 47 +++- crates/windows-platform-probes/Cargo.toml | 30 +- .../windows-platform-probes/DESIGN-NOTES.md | 6 +- .../src/bin/queue_contention.rs | 62 +++- crates/windows-platform-probes/src/lib.rs | 1 + .../src/queue_contention.rs | 42 ++- .../src/queue_contention/tests.rs | 265 ++++++++++++++++++ 7 files changed, 431 insertions(+), 22 deletions(-) create mode 100644 crates/windows-platform-probes/src/queue_contention/tests.rs diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 70b2cfbfd..22e283603 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -77,11 +77,12 @@ correctness in the archive. executable. The judgement stays with the person; the probe stops being the obstacle. **Gap:** [src/queue_contention.rs](src/queue_contention.rs) fixes every sampling parameter as a - private constant -- `PUSHES_PER_PRODUCER` (50,000) and `REPETITIONS` (5) -- and `measure()` takes - no arguments. The first move the design note prescribes on seeing a wide control is to lengthen - the span and raise the repetition count on the unchanged configuration, which is currently a - source edit and a rebuild. A control that cannot be turned is not a control, and the cheapest - diagnostic step is the one being blocked. + compile-time constant -- `PUSHES_PER_PRODUCER` (50,000) and `REPETITIONS` (5) -- and `measure()` + takes no arguments. They were made `pub` and are now printed in the report, so a captured run at + least says what produced it; but reading a constant is not setting one. The first move the design + note prescribes on seeing a wide control is to lengthen the span and raise the repetition count on + the unchanged configuration, which is still a source edit and a rebuild. A control that cannot be + turned is not a control, and the cheapest diagnostic step is the one being blocked. **Target:** `measure()` takes a settings value carrying at least the pushes-per-producer count, the repetition count, and the producer counts to sweep (`PRODUCER_COUNTS` is already public and @@ -101,10 +102,46 @@ correctness in the archive. is only interpretable with its capture parameters, and these are now among them. Making the sampling adjustable without recording it would turn one reproducibility problem into a worse one. + **Emit the dispersion, not just the median.** `median_run` currently sorts the five repetitions, + keeps the middle one, and **discards the other four** -- so `Run` carries a median with no spread, + and the ranges published in [DESIGN-NOTES.md](DESIGN-NOTES.md) exist only because they were + computed by hand outside the probe. That is the same contract failure from the other side: the + decision above requires a figure to carry "the number of runs with their dispersion", and the + instrument does not supply it. Keep at least the min and max alongside the median, and render + them. Reported by review, and correctly -- the same-code control is the evidence a reader needs + to judge any ratio here, and it is exactly what is being thrown away. + **Not in scope:** deciding why the control is wide. That is the judgement this tooling supports, and per the design note a negative result -- "lengthening and repeating do not narrow it, so the floor is here" -- is a real answer that gets recorded beside the figures. +- [ ] **M4.3** -- Close the undrained window at the start of the drained regime with a readiness + handshake, and re-measure everything that changes. + + **Gap:** the drained timings put the consumer in the same `Barrier` as the producers, which + guarantees it has *arrived* -- spawned, scheduled, past thread start-up -- but not that it reaches + its first `pop` before a producer reaches its first `push`. The barrier releases every party at + once, so a short undrained window remains at the opening of each run. It is bounded by a + scheduling quantum rather than by thread creation, which is why the barrier is still worth having, + but it is not zero, and the drained regime is defined against exactly this. + + **Target:** the consumer sets an `AtomicBool` after entering its drain loop; producers spin on it + after `gate.wait()` and before `Instant::now()`. Apply it to all four drained functions + (`time_drained_mpsc`, `time_drained_reserving`, `time_drained_permit`, `time_drained_layout`) -- + they share the defect and the three siblings currently point at the slotwise twin's comment for + the reasoning, so that comment is the one to update. + + **BLOCKER, and the reason this is queued rather than taken:** adding the handshake changes the + measurement, so every drained figure already published in + [DESIGN-NOTES.md](DESIGN-NOTES.md) -- and the withdrawal argument built on the drained control -- + becomes a measurement of different code. The item is therefore "change it *and* re-run the + seven-run sweep *and* rewrite the drained sections", not a one-line fix, and doing it mid-branch + would invalidate figures that five review rounds have already been read against. Raised rather + than silently deferred, per the PRIME DIRECTIVE. + + Reported by review against this branch; the comment at the slotwise twin now states what the + barrier actually guarantees rather than implying the window is closed. + - [ ] **M2.5** -- Make the banner describe the read the body describes. Gated by M3.1 and M3.3, both landed: establishing that the middle of three discoveries agreed diff --git a/crates/windows-platform-probes/Cargo.toml b/crates/windows-platform-probes/Cargo.toml index 66520872d..156a4d614 100644 --- a/crates/windows-platform-probes/Cargo.toml +++ b/crates/windows-platform-probes/Cargo.toml @@ -153,9 +153,6 @@ windows-namespace-request-sys = { path = "../windows-namespace-request-sys" } # the same host, in the same run, by the same harness. windows-waitable-queues = { path = "../windows-waitable-queues", features = [ "experimental-permit-claim", - # So the probe can instantiate the 128-bit layout. The 64-bit ones need no - # feature; this is the only one that costs the queue crate a dependency. - "dwcas", ] } # The long-path probe measures a length against `MAX_PATH`, and `MAX_PATH` counts # UTF-16 code units. `OsStr::len` counts Rust's platform encoding -- WTF-8 here -- @@ -178,6 +175,33 @@ wtf-string = { path = "../wtf-string" } # still the real parser's and this crate hand-writes no string walking at all. serde = { version = "1.0", optional = true } +# The 128-bit claim layout is measured only where a 128-bit exchange is a native +# instruction, and the feature that supplies it is added per-target rather than +# unconditionally. +# +# This is not a tuning choice, it is the queue crate's `D-18`/`D-37` contract: +# `dwcas` is non-default precisely so `reserving_mpsc`'s narrow word ships on +# every target while the wide one ships only where it is genuinely lock-free. +# `i686-pc-windows-msvc` has no 128-bit atomic at all, and `D-40` records that +# the workspace deliberately keeps that target supported -- so enabling `dwcas` +# from here without a target gate would propagate through feature unification and +# fail the i686 build in the queue crate (`cannot find AtomicU128 in +# portable_atomic`). Measured, not assumed: that is the exact error this gate was +# added to remove. +# +# `target_has_atomic = "128"` would be the WRONG condition -- `D-37` records that +# rustc emits it even with `cmpxchg16b` disabled. The architectures are named +# instead, matching the two the queue crate documents as natively lock-free: +# x86-64 via `cmpxchg16b`, aarch64 via `ldxp`/`stxp`. +# +# `src/queue_contention.rs` gates the `Wide` rows on the SAME condition. The two +# must agree; the source side names this comment so a reader changing one finds +# the other. +[target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies] +windows-waitable-queues = { path = "../windows-waitable-queues", features = [ + "dwcas", +] } + [dev-dependencies] # Here because [tests/a_real_report_agrees_with_itself.rs] uses `serde_json` # DIRECTLY, to empty the row's diagnostic lists by parsing and re-rendering diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index da314a0df..1dd527d3c 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -686,8 +686,10 @@ should be read as measurements of the consumer. ## The claim word's width costs 2-3x in isolation, and the drained figure is withdrawn Measured by `probe-queue-contention` on one host, `x86_64-pc-windows-msvc`. -Three apportionments of `reserving_mpsc`'s claim word: 32/32 and 16/48 over -`AtomicU64`, and 64/64 over `AtomicU128`. +Four apportionments of `reserving_mpsc`'s claim word: 32/32, 16/48 and 8/56 over +`AtomicU64`, and 64/64 over `AtomicU128`. The last is measured only where a +128-bit exchange is native -- x86-64 and aarch64 -- so on a target without one +the report carries the other three and leaves its column empty. **These were duplicated scaffolding when the measurement was taken, and they ship now.** The layouts were built as copies so the shipping crate was not diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 7019e1ba8..0b5d88082 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -10,7 +10,9 @@ //! linked and sharded MPSC shapes are ever needed, and whether `slotwise_mpsc` //! and `reserving_mpsc` should merge. See `queue_contention`'s module docs. -use windows_platform_probes::queue_contention::{PRODUCER_COUNTS, Run, measure, shapes}; +use windows_platform_probes::queue_contention::{ + PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, Run, measure, shapes, +}; use windows_platform_probes::report::emit_report; fn main() { @@ -38,6 +40,15 @@ fn render(out: &mut dyn std::fmt::Write) { "host reports {} logical processors\n", observation.logical_processors ); + // The sampling parameters are capture parameters, and a figure is only + // interpretable with them -- see D-observations-not-verdicts. The dispersion + // belongs here too and is not yet carried; M4.2 covers both. + let _ = writeln!( + out, + "sampling: {} pushes per producer, median of {} repetitions, one untimed \ + warmup pass\n", + PUSHES_PER_PRODUCER, REPETITIONS + ); let _ = writeln!( out, @@ -100,7 +111,27 @@ fn render(out: &mut dyn std::fmt::Write) { // Question 2: what does reserving_mpsc's read of `head` actually cost? let _ = writeln!( out, - "\n 2. the price of reservation (drained regime, where `head` is written)\n" + "\n 2. reserving vs slotwise, drained (where `head` is written)\n" + ); + let _ = writeln!( + out, + " The ratio is the WHOLE push path of two different shapes, not the" + ); + let _ = writeln!( + out, + " price of reserving's extra `head` load on its own: they use" + ); + let _ = writeln!( + out, + " different claim protocols, slot metadata and retry behaviour. This" + ); + let _ = writeln!( + out, + " regime is where that load is at its most expensive, so the ratio" + ); + let _ = writeln!( + out, + " bounds its contribution from above rather than isolating it.\n" ); let _ = writeln!( out, @@ -183,16 +214,37 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " apportioned differently; 64/64 is a u128 exchange (cmpxchg16b)." + " apportioned differently; 64/64 is a u128 exchange (cmpxchg16b on" + ); + let _ = writeln!( + out, + " x86-64, ldxp/stxp on aarch64), measured only where that is native." + ); + let _ = writeln!( + out, + " The three u64 rows issue the same instruction and differ only in" + ); + let _ = writeln!( + out, + " shift and mask constants, so there is no structural reason for one" + ); + let _ = writeln!( + out, + " to be slower -- but these rows time the WHOLE push path, so a" + ); + let _ = writeln!( + out, + " difference between them is not thereby noise. Read it against a" ); let _ = writeln!( out, - " The three u64 rows issue the SAME instruction, so a difference" + " control before calling it either way: the reserving_mpsc row and" ); let _ = writeln!( out, - " between them is noise or slot-metadata density, not the claim." + " the 32/32 row above are the same code, so their gap is this" ); + let _ = writeln!(out, " host's zero."); let _ = writeln!( out, " 64/64 vs 32/32 prices the double-width exchange -- what removing" diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index 3972b0373..b156f91c3 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -134,6 +134,7 @@ //! | [`doorbell_cost::measure`] | binary only | the absolute cost of `SetEvent`, a set/reset cycle and a satisfied wait against an uncontended atomic, and how much batching drives the doorbell below the push it accompanies | //! | [`doorbell_cost::measure_park_and_wake`] | asserted | that the park-and-wake handshake completes rather than deadlocking, which its first implementation did | //! | [`request_cost::measure`] | binary only | the absolute cost of preparing a path, building an owned `OpenFile`, and duplicating a handle | +//! | [`queue_contention::measure`] | binary only | what the bounded array queue's contended tail claim costs against a plain `fetch_add`, what `reserving_mpsc`'s read of the consumer's position adds, and how the claim word's bit apportionments compare -- binary only because it wants more cores than a hosted runner has, takes about a minute, and reports the two shapes as equivalent unless built with `--release` | #![cfg(windows)] #![forbid(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index d917f0324..56a5394b0 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -64,17 +64,31 @@ use std::time::Instant; use windows_waitable_queues::{permit_mpsc, reserving_mpsc, slotwise_mpsc}; -use windows_waitable_queues::reserving_mpsc::{Balanced, ClaimLayout, Enduring, Perpetual, Wide}; +use windows_waitable_queues::reserving_mpsc::{Balanced, ClaimLayout, Enduring, Perpetual}; + +/// The 128-bit layout exists only where a 128-bit exchange is native. +/// +/// The condition is duplicated in this crate's `Cargo.toml`, which adds the +/// `dwcas` feature under the same `cfg`; see the comment there for why the +/// architectures are named rather than testing `target_has_atomic = "128"`, and +/// why enabling the feature unconditionally breaks the workspace's deliberately +/// supported `i686-pc-windows-msvc` build. Changing one without the other yields +/// either a missing type or an unused feature. +#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] +use windows_waitable_queues::reserving_mpsc::Wide; + +#[cfg(test)] +mod tests; /// How many pushes each producer thread performs in one timed run. -const PUSHES_PER_PRODUCER: usize = 50_000; +pub const PUSHES_PER_PRODUCER: usize = 50_000; /// How many times each configuration is repeated; the median is reported. /// /// Odd, so the median is an observed value rather than an average of two. Five /// because these probes run on a virtual machine, where a single run can be /// perturbed by something entirely outside the process. -const REPETITIONS: usize = 5; +pub const REPETITIONS: usize = 5; /// The producer counts measured, in order. /// @@ -207,6 +221,10 @@ pub fn measure() -> Observation { isolated.push(median_run(shapes::CLAIM_PERPETUAL, producers, |count| { time_isolated_layout::(count) })); + // Gated on the architectures where a 128-bit exchange is native; see the + // `Wide` import above. `#[cfg]` governs only the statement that follows + // it, so each of the two pushes carries its own. + #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] isolated.push(median_run(shapes::CLAIM_WIDE, producers, |count| { time_isolated_layout::(count) })); @@ -220,6 +238,7 @@ pub fn measure() -> Observation { drained.push(median_run(shapes::CLAIM_PERPETUAL, producers, |count| { time_drained_layout::(count) })); + #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] drained.push(median_run(shapes::CLAIM_WIDE, producers, |count| { time_drained_layout::(count) })); @@ -467,10 +486,19 @@ fn time_drained_mpsc(producers: usize) -> Repetition { let (tx, rx) = slotwise_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); let done = Arc::new(AtomicBool::new(false)); let consumer_done = Arc::clone(&done); - // The consumer is a participant too: it is spawned first, but spawning is - // not readiness, and a consumer still starting up while producers push turns - // the opening of the run into an undrained regime -- the one thing this - // measurement is defined against. + // The consumer is a barrier participant, not merely spawned: spawning is not + // readiness, and a consumer still in thread start-up while producers push + // turns the opening of the run into an undrained regime. + // + // Be precise about what this buys, because it is less than it looks. The + // barrier guarantees the consumer has ARRIVED -- it exists, is scheduled, and + // is past start-up -- not that it reaches its first `pop` before a producer + // reaches its first `push`. A release wakes every party at once, so a short + // undrained window remains. It is bounded by a scheduling quantum rather than + // by thread creation, which is the improvement; it is not zero. Closing it + // needs a readiness flag the producers spin on, which would change the + // measurement and so obsolete every figure already published against it -- + // queued as M4.3 rather than taken mid-branch. let gate = start_barrier(producers + 1); let consumer_gate = Arc::clone(&gate); diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs new file mode 100644 index 000000000..fba85b41a --- /dev/null +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -0,0 +1,265 @@ +// Copyright (c) Mike Grier. + +//! Tests for the pure lookup helpers. +//! +//! These decide whether the report prints a ratio or `--`, so they are worth +//! testing directly; none of them needs the 65-second measurement. + +use super::*; + +/// A `Run` with everything but the fields under test held constant. +fn run(shape: &'static str, producers: usize, pushes_per_second: f64) -> Run { + Run { + shape, + producers, + nanos_per_push: if pushes_per_second > 0.0 { + 1_000_000_000.0 / pushes_per_second + } else { + 0.0 + }, + pushes_per_second, + refusals: 0, + } +} + +fn observation(isolated: Vec, drained: Vec) -> Observation { + Observation { + isolated, + drained, + logical_processors: 8, + } +} + +fn sample() -> Observation { + observation( + vec![ + run(shapes::BASELINE_FETCH_ADD, 1, 400_000_000.0), + run(shapes::BASELINE_FETCH_ADD, 4, 800_000_000.0), + run(shapes::RESERVING_MPSC, 1, 200_000_000.0), + run(shapes::RESERVING_MPSC, 4, 100_000_000.0), + run(shapes::SLOTWISE_MPSC, 1, 100_000_000.0), + ], + vec![ + run(shapes::RESERVING_MPSC, 1, 40_000_000.0), + run(shapes::RESERVING_MPSC, 4, 10_000_000.0), + ], + ) +} + +#[test] +fn find_returns_the_row_matching_both_shape_and_producer_count() { + let observed = sample(); + let found = observed + .find(&observed.isolated, shapes::RESERVING_MPSC, 4) + .expect("the row is present"); + assert_eq!(found.shape, shapes::RESERVING_MPSC); + assert_eq!(found.producers, 4); +} + +#[test] +fn find_distinguishes_rows_that_share_a_shape() { + let observed = sample(); + let one = observed + .find(&observed.isolated, shapes::RESERVING_MPSC, 1) + .expect("present"); + let four = observed + .find(&observed.isolated, shapes::RESERVING_MPSC, 4) + .expect("present"); + assert_ne!(one.pushes_per_second, four.pushes_per_second); +} + +#[test] +fn find_distinguishes_rows_that_share_a_producer_count() { + let observed = sample(); + let reserving = observed + .find(&observed.isolated, shapes::RESERVING_MPSC, 1) + .expect("present"); + let slotwise = observed + .find(&observed.isolated, shapes::SLOTWISE_MPSC, 1) + .expect("present"); + assert_ne!(reserving.pushes_per_second, slotwise.pushes_per_second); +} + +#[test] +fn find_returns_none_for_a_shape_that_was_not_measured() { + let observed = sample(); + assert!( + observed + .find(&observed.isolated, shapes::CLAIM_WIDE, 1) + .is_none() + ); +} + +#[test] +fn find_returns_none_for_a_producer_count_that_was_not_measured() { + let observed = sample(); + assert!( + observed + .find(&observed.isolated, shapes::RESERVING_MPSC, 32) + .is_none() + ); +} + +/// The regime is a parameter, so the same shape and count must not leak across. +#[test] +fn find_reads_only_the_regime_it_is_given() { + let observed = sample(); + let isolated = observed + .find(&observed.isolated, shapes::RESERVING_MPSC, 1) + .expect("present in isolated"); + let drained = observed + .find(&observed.drained, shapes::RESERVING_MPSC, 1) + .expect("present in drained"); + assert_ne!(isolated.pushes_per_second, drained.pushes_per_second); + assert!( + observed + .find(&observed.drained, shapes::SLOTWISE_MPSC, 1) + .is_none(), + "slotwise was measured only in the isolated regime here" + ); +} + +#[test] +fn find_on_an_empty_regime_is_none_rather_than_a_panic() { + let observed = observation(Vec::new(), Vec::new()); + assert!( + observed + .find(&observed.isolated, shapes::RESERVING_MPSC, 1) + .is_none() + ); +} + +#[test] +fn scaling_divides_the_many_producer_rate_by_the_one_producer_rate() { + let observed = sample(); + // baseline: 800M at four producers against 400M at one. + let scaled = observed + .scaling(&observed.isolated, shapes::BASELINE_FETCH_ADD, 4) + .expect("both rows present"); + assert!( + (scaled - 2.0).abs() < f64::EPSILON, + "expected 2.0, got {scaled}" + ); +} + +/// The direction matters: a contended claim scales *below* one, and reporting +/// the reciprocal would turn the finding upside down. +#[test] +fn scaling_below_one_means_more_producers_pushed_fewer_items() { + let observed = sample(); + let scaled = observed + .scaling(&observed.isolated, shapes::RESERVING_MPSC, 4) + .expect("both rows present"); + assert!( + (scaled - 0.5).abs() < f64::EPSILON, + "expected 0.5, got {scaled}" + ); + assert!(scaled < 1.0, "this is what a contended claim looks like"); +} + +#[test] +fn scaling_at_one_producer_is_one_by_construction() { + let observed = sample(); + let scaled = observed + .scaling(&observed.isolated, shapes::RESERVING_MPSC, 1) + .expect("the one-producer row is present"); + assert!( + (scaled - 1.0).abs() < f64::EPSILON, + "expected 1.0, got {scaled}" + ); +} + +#[test] +fn scaling_is_none_when_the_one_producer_row_is_missing() { + let observed = observation( + vec![run(shapes::RESERVING_MPSC, 4, 100_000_000.0)], + Vec::new(), + ); + assert!( + observed + .scaling(&observed.isolated, shapes::RESERVING_MPSC, 4) + .is_none(), + "without the one-producer row there is nothing to scale against" + ); +} + +#[test] +fn scaling_is_none_when_the_many_producer_row_is_missing() { + let observed = observation( + vec![run(shapes::RESERVING_MPSC, 1, 200_000_000.0)], + Vec::new(), + ); + assert!( + observed + .scaling(&observed.isolated, shapes::RESERVING_MPSC, 32) + .is_none() + ); +} + +#[test] +fn scaling_is_none_for_a_shape_absent_from_the_regime() { + let observed = sample(); + assert!( + observed + .scaling(&observed.drained, shapes::BASELINE_FETCH_ADD, 4) + .is_none() + ); +} + +/// A zero denominator yields a non-finite value rather than a panic, so the +/// renderer's own guard is what decides the cell. This pins the contract that +/// `scaling` does not itself panic on degenerate input. +#[test] +fn scaling_against_a_zero_rate_is_non_finite_rather_than_a_panic() { + let observed = observation( + vec![ + run(shapes::RESERVING_MPSC, 1, 0.0), + run(shapes::RESERVING_MPSC, 4, 100_000_000.0), + ], + Vec::new(), + ); + let scaled = observed + .scaling(&observed.isolated, shapes::RESERVING_MPSC, 4) + .expect("both rows are present"); + assert!(!scaled.is_finite(), "expected non-finite, got {scaled}"); +} + +/// Every shape name must be distinct, or `find` would return whichever row +/// happened to come first and two columns would silently show one measurement. +#[test] +fn every_shape_name_is_distinct() { + let names = [ + shapes::BASELINE_FETCH_ADD, + shapes::SLOTWISE_MPSC, + shapes::RESERVING_MPSC, + shapes::PERMIT_MPSC, + shapes::CLAIM_NARROW, + shapes::CLAIM_DEEP, + shapes::CLAIM_PERPETUAL, + shapes::CLAIM_WIDE, + ]; + for (index, name) in names.iter().enumerate() { + assert!( + !names[..index].contains(name), + "{name} appears more than once" + ); + } +} + +/// `capacity_for` must leave room for every push, or the isolated regime would +/// refuse and stop being the regime it claims to be. +#[test] +fn capacity_for_exceeds_the_pushes_every_producer_count_will_make() { + for &producers in PRODUCER_COUNTS { + let capacity = capacity_for(producers); + let pushes = producers * PUSHES_PER_PRODUCER; + assert!( + capacity > pushes, + "{producers} producers push {pushes} but capacity is {capacity}" + ); + assert!( + capacity.is_power_of_two(), + "{capacity} must be a power of two" + ); + } +} From 40226d7bfce76f41fd69805b177549ad9e4b6dc9 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 19:19:27 -0700 Subject: [PATCH 012/139] docs(waitable-queues): the last sites of the withdrawn claim, and a stale figure Two more sites still told callers a deeper claim-word layout was measured to cost nothing, and my previous sweeps missed both for reasons worth recording: - `src/reserving_mpsc.rs`, the `Wide` rustdoc: "[`Perpetual`] reaches about twenty years on a plain `AtomicU64` at no measured cost". The phrase is LINE-WRAPPED as "at no" / "measured cost", so a line-based grep for "no measured cost" cannot match it. Every sweep I ran was line-based. - `Cargo.toml`, the `dwcas` feature comment: same claim, and I had never searched manifests at all -- only `*.rs` and `*.md`. A flattening multiline scan finds both immediately. That is the instrument this class of sweep needs; a line-oriented one silently under-reports. Also corrects a figure the re-measurement superseded. Four sites said the 128-bit exchange measured "2-3x" slower (the README said "2-4x" in one place and "2-3x" in another, disagreeing with itself). The seven-run measurement puts the isolated ratio at roughly 1.1x at one or two producers rising to about 3.8x at thirty-two, and inside the same-code control when drained -- so "2-3x" overstates the low end and understates the high end. Replaced with the range and its regime dependence in README.md, src/lib.rs, src/reserving_mpsc.rs, Cargo.toml, and D-41. Reported by review, which caught every one of these. Verified: 13 doctests pass including the compiled README; 304 lib tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/Cargo.toml | 5 +++-- crates/windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 8 +++++--- crates/windows-waitable-queues/src/lib.rs | 3 ++- .../windows-waitable-queues/src/reserving_mpsc.rs | 15 +++++++++------ 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/windows-waitable-queues/Cargo.toml b/crates/windows-waitable-queues/Cargo.toml index f77ddb42e..a0cd190b8 100644 --- a/crates/windows-waitable-queues/Cargo.toml +++ b/crates/windows-waitable-queues/Cargo.toml @@ -58,8 +58,9 @@ experimental-permit-claim = [] # target and the build fails naming it. # # Most callers should not need this. `Perpetual` reaches roughly twenty years -# before its claim position recurs, on a plain `AtomicU64` at no measured cost, -# whereas the 128-bit exchange measured 2-3x slower on the claim itself. See +# before its claim position recurs, on a plain `AtomicU64`; what that costs in +# throughput is not established, and the 128-bit exchange measured roughly 1.1x +# to 3.8x slower on the claim itself depending on producer count. See # `ClaimLayout` for the comparison. dwcas = ["dep:portable-atomic"] diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 80e61c738..9c98bed7e 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` up to eight producers and sit near 1.26x at sixteen and thirty-two, against a same-code control reaching 1.12x -- which is one host declining to call it, not a cost. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and it measured 2-3x slower on the claim; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` up to eight producers and sit near 1.26x at sixteen and thirty-two, against a same-code control reaching 1.12x -- which is one host declining to call it, not a cost. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and it measured roughly 1.1x at one or two producers rising to about 3.8x at thirty-two in the isolated regime, while falling inside the same-code control when drained; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index b547e693f..63c60c04c 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -146,7 +146,8 @@ one to be slower -- but **what that costs in throughput is not established**: a probe comparing them found them indistinguishable up to eight producers and near 1.26x at sixteen and thirty-two, on one host, against a same-code control that itself reached 1.12x. `Wide` is a separate matter: it needs a 128-bit exchange, -which measured 2-3x slower on the claim in isolation, and it is the only thing in +which measured roughly 1.1x at one or two producers rising to about 3.8x at +thirty-two in isolation, and it is the only thing in this crate that costs a third-party dependency. The default remains `Balanced` so that no existing caller's behaviour changed @@ -211,8 +212,9 @@ Both are off by default, and the default build depends on `windows-sys` alone. dependency: Rust's standard library has no 128-bit atomic -- `core::sync::atomic` stops at 64 bits -- so the double-width compare-and-swap comes from `portable-atomic`. Most callers do not need it; `Perpetual` reaches roughly -twenty years before its claim position recurs with no dependency and no measured -cost, while the 128-bit exchange measured 2-4x slower on the claim itself. Take +twenty years before its claim position recurs with no dependency, though what +that costs in throughput is not established, while the 128-bit exchange measured +roughly 1.1x to 3.8x slower on the claim itself depending on producer count. Take it when you want the recurrence gone as a guarantee rather than deferred by an argument about deployment lifetimes. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index b5bf5218b..635d8a5b3 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -115,7 +115,8 @@ //! eight producers and near 1.26x at sixteen and thirty-two, on one host, //! against a same-code control that itself reached 1.12x. `Wide` is a separate //! matter: it needs a 128-bit exchange, which -//! measured 2-3x slower on the claim in isolation, and it is the only thing in +//! measured roughly 1.1x at one or two producers rising to about 3.8x at +//! thirty-two in isolation, and it is the only thing in //! this crate //! that costs a third-party dependency. Prefer `Perpetual` unless you want the //! guarantee rather than the twenty years. diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 5bcbd3398..efcbec37f 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -528,12 +528,15 @@ impl ClaimLayout for Perpetual { /// pushes to recur, which no deployment reaches -- not "not for twenty years", /// but not at all. /// -/// **Read the cost before choosing it.** The 128-bit exchange measured 2-3x -/// slower than a `u64` one on the claim itself, and the penalty grows with -/// producer count; against a draining consumer the difference is much smaller. -/// [`Perpetual`] reaches about twenty years on a plain `AtomicU64` at no -/// measured cost, so this is worth taking only when a guarantee is wanted in -/// place of an argument about deployment lifetimes. +/// **Read the cost before choosing it.** The 128-bit exchange measured slower +/// than a `u64` one on the claim itself -- about 1.1x at one or two producers +/// rising to roughly 3.8x at thirty-two on one x86-64 host, so the penalty grows +/// with producer count; against a draining consumer the difference fell inside +/// that host's same-code control and could not be called at all. +/// [`Perpetual`] reaches about twenty years on a plain `AtomicU64`, and **what +/// that costs in throughput is not established** -- see [`ClaimLayout`]. So this +/// is worth taking when a guarantee is wanted in place of an argument about +/// deployment lifetimes, not because the narrow alternative is known to be free. /// /// The reservation ceiling is [`u32::MAX`] rather than the 64 bits the field /// could hold, because the count is reported to callers as a `u32`. From 56693ac80c1683bd757afd94fbfe0af120eaed3b Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 19:27:43 -0700 Subject: [PATCH 013/139] fix(platform-probes): warn in the report when the build cannot measure Three more findings from the same review, all in earlier rounds' suppressed comments. **A debug run emitted an authoritative-looking report.** The notes have always said a debug build reports the two MPSC shapes as equivalent -- a confident wrong answer rather than a loss of precision -- but nothing said so at the point of use. Measured again on this branch to be sure it still holds: debug puts `slotwise_mpsc` at 224.2 and `reserving_mpsc` at 229.9 ns/push at sixteen producers, against 222.4 and 52.3 in release. The warning goes in the report rather than in a refusal to run, because it has to travel with a captured report. Whoever pastes these numbers into a document is the person who needs to see it; a binary that refused would only tell the person who already had the terminal open. Verified in both directions: the banner appears in a debug run and is absent from a release run. **The warmup comment claimed something the warmup does not do.** It said the untimed pass faults in "the" allocation's pages -- but every call to `timer` builds and drops its own queue, so the pass cannot pre-touch any allocation a timed repetition will use. What it actually warms is the process: the allocator's size class, the OS page cache, the instruction cache, branch predictors. Corrected to say that, since the difference matters to anyone reasoning about what the first repetition costs. **The isolated-regime attribution was already fixed** before this review round -- the module doc now says explicitly that the curve is the whole push path and not the compare-and-swap alone, and that an earlier draft claimed otherwise. Recorded here because the comment reported it as outstanding. Process note: the first attempt to verify the banner measured a STALE debug binary -- an earlier `cargo build` had failed on an "Access is denied" incremental-compilation error, leaving a binary older than the source, and the banner was correctly absent from it. Caught by comparing binary mtime against source mtime, which is the check worth keeping: a probe that reports on itself is exactly where a stale binary is hardest to notice. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 22 +++++++++++++++++++ .../src/queue_contention.rs | 10 +++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 0b5d88082..fec40dee9 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -49,6 +49,28 @@ fn render(out: &mut dyn std::fmt::Write) { warmup pass\n", PUSHES_PER_PRODUCER, REPETITIONS ); + // A debug build does not merely lose precision here: the un-inlined overhead + // swamps the cache-coherence effects that ARE the finding, and the two MPSC + // shapes come out indistinguishable -- a confident wrong answer. The banner + // goes in the report rather than in a refusal to run, because it has to + // travel with a captured report: whoever pastes these numbers somewhere is + // the person who needs to see it, and a binary that refused would tell only + // the person who already had the terminal open. + if cfg!(debug_assertions) { + let _ = writeln!( + out, + "!! DEBUG BUILD -- THESE NUMBERS ARE NOT A MEASUREMENT !!" + ); + let _ = writeln!( + out, + "!! Un-inlined overhead swamps the effect being measured, and the" + ); + let _ = writeln!( + out, + "!! MPSC shapes report as equivalent when they are not. Rebuild" + ); + let _ = writeln!(out, "!! with --release before reading anything below.\n"); + } let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 56a5394b0..672cf206c 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -264,8 +264,14 @@ fn median_run( producers: usize, mut timer: impl FnMut(usize) -> Repetition, ) -> Run { - // One untimed pass first: the first touch of a fresh allocation faults - // pages in, and that cost belongs to the allocator rather than the queue. + // One untimed pass first. Be exact about what this does and does not warm: + // every call to `timer` builds and drops its OWN queue, so this does not + // pre-touch the allocation any timed repetition will use. What it does warm + // is the process -- the allocator's size class, the OS page cache, the + // instruction cache, and the branch predictors -- which is why the first + // timed repetition is no longer an outlier. An earlier comment here claimed + // it faulted in "the" allocation, which is not true of an allocation made + // fresh each pass. Found by a review. let _ = timer(producers); let mut results: Vec = (0..REPETITIONS).map(|_| timer(producers)).collect(); From b23d70eef143dda627be5c3ffe5f6d188458f6be Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 20:07:49 -0700 Subject: [PATCH 014/139] fix(platform-probes): stop the attribution correction stopping at one site Two real code findings and six restatements of a fact I had already corrected once -- the second is the more interesting failure. **`format_scaling` rendered non-finite values.** `format_ratio` guards its denominator and documents why; its sibling did not, so a degenerate observation would print `infx` in a column of measurements. The test added in 2d29bee (`scaling_against_a_zero_rate_is_non_finite_rather_than_a_panic`) documents precisely the input that reaches it, so the suite was carrying the evidence for a bug it did not check. Guard added at the display layer, which is where it belongs -- `scaling` is arithmetic and is deliberately allowed to return the non-finite value; the test's doc comment now names `format_scaling` so the two stay tied. **`available_parallelism` was labelled as the host count and mapped failure to zero.** It is the process-available estimate: an affinity mask or job object narrows it, so the report could contradict the topology banner three lines above it, and a failed query printed "host reports 0 logical processors" -- a value no host has, rendered with the same confidence as a measured one. The field is now `available_parallelism: Option`, and the report says "processors available to this process", or "unknown (the query failed)". It is worth reporting on its own terms because it decides whether a producer count oversubscribes THIS run, which the banner cannot answer. **The attribution fix had not propagated.** 2d29bee corrected the renderer's claim that the reserving/slotwise ratio prices `reserving_mpsc`'s `head` read. The same claim survived in six other places, including a later paragraph of the same renderer function: - the module doc's drained bullet, which still said "the one that can price" - two `///` comments describing timers as "the regime that isolates the claim" - the crate inventory row in `lib.rs` - `DESIGN-NOTES.md`'s section opening, and a second site calling drained "the only regime that can price" - `bin/queue_contention.rs`, ~10 lines below the paragraph I had just fixed All now say the ratio BOUNDS that read's contribution from above rather than pricing it, because the comparison is between two complete push paths differing in claim protocol and slot metadata as well as in that one load. Also corrected, same class: the warmup claim restated in `DESIGN-NOTES.md` and `DESIGN-RATIONALE.md` after the code comment was fixed in 56693ac, and a section heading still reading "costs 2-3x in isolation" against a body reporting 3.45x/3.81x. The sweep that found the stragglers was claim-based and multiline -- matching the proposition across line wraps and across `.rs`, `.md` and `.toml` -- rather than matching the phrasings I happened to have written. Every phrase-based sweep in this branch has under-reported; this one found four sites the targeted fixes had missed, including two of my own corrections that were themselves incomplete. Verified: fmt and clippy clean; 16 queue_contention tests; release run shows the process-available label, no `host reports`, no non-finite cells, and the corrected attribution text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 32 +++++++++---- .../DESIGN-RATIONALE.md | 5 +- .../src/bin/queue_contention.rs | 43 +++++++++++++---- crates/windows-platform-probes/src/lib.rs | 2 +- .../src/queue_contention.rs | 47 +++++++++++++++---- .../src/queue_contention/tests.rs | 10 ++-- 6 files changed, 104 insertions(+), 35 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 1dd527d3c..7e548bf83 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -119,8 +119,12 @@ machine, and before suspecting the probe -- each of those changes what is being measured, and a change made before the cheap check cannot be evaluated. **A warmup pass separates transient cost from ongoing noise, and the two are -different things.** This probe already discards one untimed pass, but only to -fault in a fresh allocation's pages. Cold caches, branch predictors, and CPU +different things.** This probe already discards one untimed pass -- though not for +the reason an earlier version of this paragraph gave. Every timed repetition +builds and drops its own queue, so the discarded pass cannot fault in any +allocation a timed pass will use; what it warms is process state, the allocator's +size class, the OS page cache, the instruction cache and the branch predictors. +Cold caches, predictors, and CPU frequency ramp are the same *kind* of cost -- one-time, front-loaded, not a property of the steady state -- and lengthening the timed span dilutes them whether or not a warmup removes them. @@ -619,9 +623,13 @@ comparison exists to classify correctly: a red build that is **not** a finding. ## The queue-contention probe, and why it must not run in the CI probe job `probe-queue-contention` measures two things a design decision is waiting on: whether the bounded -array queue's tail claim contends badly enough to justify the linked and sharded MPSC shapes, and -what [`reserving_mpsc`](../windows-waitable-queues/src/reserving_mpsc.rs)'s extra read of the -consumer's position actually costs. +array queue's tail claim contends badly enough to justify the linked and sharded MPSC shapes, and an +upper bound on what [`reserving_mpsc`](../windows-waitable-queues/src/reserving_mpsc.rs)'s extra read +of the consumer's position costs. **A bound rather than a price**, because the only ratio available +is between two complete push paths: `reserving_mpsc` and `slotwise_mpsc` differ in claim protocol and +slot metadata as well as in that one load, so all of it sits inside the same number. The drained +regime is where the read is most expensive, which is what makes the bound tight enough to be worth +having. **The checklists carrying those decisions are not in this repository yet** -- they arrive with the rest of the queue work -- so this note deliberately names the QUESTIONS rather than linking to items @@ -674,8 +682,10 @@ whatever curve appears against N is the producer side alone, with no consumer tr the claim alone -- what is timed is each shape's whole push path, tail claim and slot write and publication and doorbell together, so a difference here is a difference in PUSH COST rather than evidence about the claim on its own. **Drained** runs a consumer popping -continuously, which is the only regime that can price `reserving_mpsc`'s read of `head` -- that read is +continuously, which is the regime in which `reserving_mpsc`'s read of `head` is most expensive -- that +read is cheap until a consumer is *writing* the line, and measuring it in isolation would report it as free. +It bounds that read rather than pricing it, because the ratio is between two complete push paths. The drained regime has a **single** consumer, because that is what MPSC means, so at high producer counts it becomes consumer-bound and a plateau there says nothing about the claim. Each row carries the refusal @@ -683,7 +693,7 @@ count from the queue's own `Observable` counters precisely so that is visible as mistaken for contention: the sixteen- and thirty-two-producer drained rows show millions of refusals and should be read as measurements of the consumer. -## The claim word's width costs 2-3x in isolation, and the drained figure is withdrawn +## The claim word's width costs 1.1x to 3.8x in isolation, and the drained figure is withdrawn Measured by `probe-queue-contention` on one host, `x86_64-pc-windows-msvc`. Four apportionments of `reserving_mpsc`'s claim word: 32/32, 16/48 and 8/56 over @@ -728,11 +738,13 @@ buys is not in dispute: the recurrence moves from 2^32 to 2^48, from about 37 seconds of sustained maximum-rate pushing to about 28 days. **Widening the word is not free in the isolated regime, and the drained figure -below does not survive the re-measurement.** Isolated, where the claim is the -only thing happening, `cmpxchg16b` costs 2-3x and the penalty *grows* with +below does not survive the re-measurement.** Isolated, where no consumer touches +the queue, `cmpxchg16b` cost 2-3x on the stand-in and the penalty *grows* with contention; that is the one conclusion in this section the seven-run re-measurement strengthened, to 3.45x and 3.81x at sixteen and thirty-two -producers. The drained figure of 5-12% is **withdrawn** -- not because the +producers. (These are shares of total push cost, not of the exchange: the +isolated regime times the whole push path, and only the layout differs between +these rows.) The drained figure of 5-12% is **withdrawn** -- not because the number moved, but because nothing was measuring whether it meant anything. The re-measured drained 128-bit rows run 2-13%, which resembles the old figure closely enough to look like confirmation, while every one of them sits inside a diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 81948048f..3b1a5f518 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -644,8 +644,9 @@ it continues for the whole run and no amount of warming touches it. Removing the transients therefore does not hide the inherent floor, it uncovers it, which is why the expected signature is a spread that narrows as warming and length increase and then stops narrowing. The plateau is the inherent part. This probe -already discards one untimed pass, though only for the allocation's pages, so -part of this is done and the rest is unmeasured. +already discards one untimed pass -- which warms process and allocator state +rather than the timed allocation, since each repetition builds its own queue -- +so part of this is done and the rest is unmeasured. **The calibration is the part worth writing down, because it is not obvious and it cuts both ways.** A spread like this in a benchmark or a marketing document diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index fec40dee9..ffa404d09 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -35,11 +35,17 @@ fn render(out: &mut dyn std::fmt::Write) { let _ = writeln!(out, "== does the array queue's tail claim contend? ==\n"); let observation = measure(); - let _ = writeln!( - out, - "host reports {} logical processors\n", - observation.logical_processors - ); + // `available_parallelism`, not the host count -- an affinity mask or job + // object narrows it, and saying "host reports" under either would contradict + // the banner three lines up. The host's shape is already there; this is what + // decides whether a producer count oversubscribes THIS run. + let _ = match observation.available_parallelism { + Some(count) => writeln!(out, "processors available to this process: {count}\n"), + None => writeln!( + out, + "processors available to this process: unknown (the query failed)\n" + ), + }; // The sampling parameters are capture parameters, and a figure is only // interpretable with them -- see D-observations-not-verdicts. The dispersion // belongs here too and is not yet carried; M4.2 covers both. @@ -185,15 +191,23 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " `slotwise_mpsc` does not, which is the entire reason they ship as" + " `slotwise_mpsc` does not. This regime is where that read is at its" + ); + let _ = writeln!( + out, + " most expensive, because a consumer is writing the line being read" ); let _ = writeln!( out, - " two shapes. This regime is the one that can price that read," + " -- which is why the ratio bounds its cost from above. It does not" ); let _ = writeln!( out, - " because a consumer is writing the line being read." + " price it: the two shapes also differ in claim protocol and slot" + ); + let _ = writeln!( + out, + " metadata, and all of that is inside the same number." ); let _ = writeln!( out, @@ -374,8 +388,19 @@ fn render_table(out: &mut dyn std::fmt::Write, runs: &[Run]) { } } +/// A scaling factor, or `--` when it is missing or not a number. +/// +/// Guards non-finite values for the same reason [`format_ratio`] guards its +/// denominator, and the guard belongs here rather than in `scaling`: a shape +/// whose one-producer row reports zero makes the quotient infinite, and +/// `infx` in a column of measurements reads as a measurement. `scaling` is +/// deliberately allowed to return the non-finite value -- it is arithmetic, not +/// a renderer -- so the display layer is where it has to be caught. fn format_scaling(scaling: Option) -> String { - scaling.map_or_else(|| "--".to_owned(), |value| format!("{value:.2}x")) + match scaling { + Some(value) if value.is_finite() => format!("{value:.2}x"), + _ => "--".to_owned(), + } } /// `numerator / denominator` as a cost ratio, or `--` when either is missing. diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index b156f91c3..3bad385c4 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -134,7 +134,7 @@ //! | [`doorbell_cost::measure`] | binary only | the absolute cost of `SetEvent`, a set/reset cycle and a satisfied wait against an uncontended atomic, and how much batching drives the doorbell below the push it accompanies | //! | [`doorbell_cost::measure_park_and_wake`] | asserted | that the park-and-wake handshake completes rather than deadlocking, which its first implementation did | //! | [`request_cost::measure`] | binary only | the absolute cost of preparing a path, building an owned `OpenFile`, and duplicating a handle | -//! | [`queue_contention::measure`] | binary only | what the bounded array queue's contended tail claim costs against a plain `fetch_add`, what `reserving_mpsc`'s read of the consumer's position adds, and how the claim word's bit apportionments compare -- binary only because it wants more cores than a hosted runner has, takes about a minute, and reports the two shapes as equivalent unless built with `--release` | +//! | [`queue_contention::measure`] | binary only | what the bounded array queue's contended tail claim costs against a plain `fetch_add`, an upper bound on what `reserving_mpsc`'s read of the consumer's position adds (the ratio is between two complete push paths, so it bounds that read rather than isolating it), and how the claim word's bit apportionments compare -- binary only because it wants more cores than a hosted runner has, takes about a minute, and reports the two shapes as equivalent unless built with `--release` | #![cfg(windows)] #![forbid(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 672cf206c..586c58b7f 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -41,10 +41,17 @@ //! reading more out of the number than is in it. Found by a review. //! //! - **Drained** -- a consumer popping continuously while the producers push. -//! This is the one that can price `reserving_mpsc`, because its producer reads -//! `head`, and `head` is only expensive to read when a consumer is *writing* -//! it. Measured in isolation that read hits a clean, shared line and looks -//! free -- which would be a confident wrong answer. +//! This is the regime in which `reserving_mpsc`'s read of `head` is at its +//! most expensive, because `head` is only costly to read when a consumer is +//! *writing* it. Measured in isolation that read hits a clean, shared line and +//! looks free -- which would be a confident wrong answer. +//! +//! **It does not isolate that read either**, for the same reason the isolated +//! regime does not isolate the claim: the ratio is between two complete push +//! paths, and `reserving_mpsc` and `slotwise_mpsc` differ in claim protocol and +//! slot metadata as well as in that one load. So the ratio **bounds the read's +//! contribution from above** rather than pricing it. Found by a review, which +//! is also how the isolated bullet above got its correction. //! //! # What is deliberately not claimed //! @@ -155,8 +162,19 @@ pub struct Observation { pub isolated: Vec, /// Producers timed against a continuously draining consumer. pub drained: Vec, - /// Logical processors the host reports. - pub logical_processors: usize, + /// Processors available to **this process**, when it could be determined. + /// + /// This is `available_parallelism`, which is the process-available estimate + /// and not the host's logical-processor count: an affinity mask or a job + /// object narrows it, so under either it is legitimately smaller than the + /// banner's `16p`. It is reported because it is what decides whether a + /// producer count oversubscribes *this run*, which is the question a reader + /// of these rows actually has; the host's own shape is already on the banner. + /// + /// `None` when the query failed. An earlier version mapped failure to `0`, + /// which the report then printed as a zero-processor host -- a value no host + /// has, presented with the same confidence as a measured one. + pub available_parallelism: Option, } impl Observation { @@ -247,7 +265,9 @@ pub fn measure() -> Observation { Observation { isolated, drained, - logical_processors: thread::available_parallelism().map_or(0, std::num::NonZeroUsize::get), + available_parallelism: thread::available_parallelism() + .ok() + .map(std::num::NonZeroUsize::get), } } @@ -447,7 +467,11 @@ fn time_isolated_reserving(producers: usize) -> Repetition { (elapsed, refusals) } -/// The experimental permit claim, in the regime that isolates the claim itself. +/// The experimental permit claim, with no consumer and no possibility of refusal. +/// +/// Not "the regime that isolates the claim", which an earlier wording said: this +/// times the whole push path, including slot metadata, the item write, +/// publication and the doorbell's fence. See the module header. /// /// A line-for-line twin of [`time_isolated_reserving`] with one shape /// substituted. Deliberately not factored into a generic over the two, which @@ -701,7 +725,12 @@ fn time_drained_permit(producers: usize) -> Repetition { (elapsed, refusals) } -/// One claim-word layout, in the regime that isolates the claim. +/// One claim-word layout, with no consumer and no possibility of refusal. +/// +/// As with the other isolated timers, this is the whole push path and not the +/// claim word alone; only the layout differs between these rows, so a difference +/// is still attributable to the layout, but its magnitude is a share of total +/// push cost rather than of the exchange. /// /// **Generic over the layout, where [`time_isolated_permit`] is deliberately /// duplicated, and the difference is the point.** That twin compares two diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index fba85b41a..053032a2e 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -26,7 +26,7 @@ fn observation(isolated: Vec, drained: Vec) -> Observation { Observation { isolated, drained, - logical_processors: 8, + available_parallelism: Some(8), } } @@ -206,9 +206,11 @@ fn scaling_is_none_for_a_shape_absent_from_the_regime() { ); } -/// A zero denominator yields a non-finite value rather than a panic, so the -/// renderer's own guard is what decides the cell. This pins the contract that -/// `scaling` does not itself panic on degenerate input. +/// A zero denominator yields a non-finite value rather than a panic. That is +/// deliberate -- `scaling` is arithmetic, not a renderer -- and it is why +/// `format_scaling` in the binary must filter non-finite values before +/// formatting, or a degenerate observation prints `infx` in a column of +/// measurements. This test pins the half of that contract the library owns. #[test] fn scaling_against_a_zero_rate_is_non_finite_rather_than_a_panic() { let observed = observation( From 88df746a47e14d9df5680009fcee0193ec3b98ea Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 20:31:27 -0700 Subject: [PATCH 015/139] fix(platform-probes): the "bounds it" correction was itself an over-claim Round 9 corrected the renderer's claim that the reserving/slotwise ratio PRICES `reserving_mpsc`'s `head` read, replacing it with "bounds it from above". Review round 11 points out that the replacement is also false, and the arithmetic is plain: writing R and S for the two shapes' total push costs, R - S is that read plus the differences in claim protocol, slot metadata and retry behaviour, and those terms are not ordered. Measured on this host, isolated at sixteen producers: `reserving_mpsc` 52.8 ns against `slotwise_mpsc` 260.2 -- R - S is about -207 ns. A read costs at least nothing, so a negative difference bounds it from above in no sense at all. The more modest-sounding word concealed an ordering assumption the data contradicts. Corrected in all five sites that carried it: the module doc, the renderer (two separate paragraphs), the `lib.rs` inventory row, and `DESIGN-NOTES.md`. They now say the ratio is an end-to-end comparison of two shapes in the regime where the read is most expensive, and nothing finer; isolating the read would need a matched control this probe does not have. **A second over-claim, caught by running the probe rather than by reading it.** The first version of this fix had the report assert "reserving_mpsc is the FASTER shape here" inside the DRAINED section, to justify the negative sign. The run disagreed: drained at sixteen producers gave `reserving_mpsc` 273.3 against `slotwise_mpsc` 243.1 -- slower -- while the seven-run medians had it faster. The drained ordering is not stable across runs on one host, so the sign is not a finding either. The text now points at the ISOLATED table, where the gap is large and consistent, as the demonstration that the non-read terms can be big and negative, and asserts no ordering for the drained regime. Also from the same review: - **`capacity 1024` was hard-coded in the renderer** while the timers used `DRAINED_CAPACITY`. A duplicated constant that can drift into mislabelling every drained row. `DRAINED_CAPACITY` is now `pub` and interpolated. - **"roughly 1.1x at one or two producers" flattened two different numbers.** The seven-run table records 1.37x at one producer and 1.13x at two, so the summary understated the one-producer result, and "rising to 3.8x" misdescribed a curve that dips at two producers before climbing. Replaced in five sites with the shape the table actually shows: 1.1x-1.4x up to four producers, 1.8x at eight, 3.5x-3.8x at sixteen and thirty-two. - **A fourth restatement of the warmup claim**, in the capture-parameters paragraph of `DESIGN-NOTES.md`. The code comment, one design note and the rationale were corrected in earlier rounds; this one said the untimed pass "exists to fault in the fresh allocation's pages", which it cannot, since every repetition builds its own queue. Verified: fmt and clippy clean; 13 doctests including the compiled README; release run against a binary confirmed newer than source, with the rendered claim checked against that same run's isolated rows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 30 ++++++++++++------- .../src/bin/queue_contention.rs | 30 ++++++++++++++----- crates/windows-platform-probes/src/lib.rs | 2 +- .../src/queue_contention.rs | 22 +++++++++----- crates/windows-waitable-queues/Cargo.toml | 5 ++-- .../windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 7 +++-- crates/windows-waitable-queues/src/lib.rs | 4 +-- .../src/reserving_mpsc.rs | 9 +++--- 9 files changed, 74 insertions(+), 37 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 7e548bf83..50afa4998 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -623,13 +623,19 @@ comparison exists to classify correctly: a red build that is **not** a finding. ## The queue-contention probe, and why it must not run in the CI probe job `probe-queue-contention` measures two things a design decision is waiting on: whether the bounded -array queue's tail claim contends badly enough to justify the linked and sharded MPSC shapes, and an -upper bound on what [`reserving_mpsc`](../windows-waitable-queues/src/reserving_mpsc.rs)'s extra read -of the consumer's position costs. **A bound rather than a price**, because the only ratio available -is between two complete push paths: `reserving_mpsc` and `slotwise_mpsc` differ in claim protocol and -slot metadata as well as in that one load, so all of it sits inside the same number. The drained -regime is where the read is most expensive, which is what makes the bound tight enough to be worth -having. +array queue's tail claim contends badly enough to justify the linked and sharded MPSC shapes, and how +[`reserving_mpsc`](../windows-waitable-queues/src/reserving_mpsc.rs) and `slotwise_mpsc` compare end +to end in the regime where `reserving_mpsc`'s extra read of the consumer's position is most expensive. + +**An end-to-end comparison, and deliberately nothing finer.** Two earlier wordings of this sentence +were both wrong: the first said the probe *prices* that read, the second said it *bounds* it from +above. Neither holds. Writing `R` and `S` for the two shapes' total push costs, `R - S` contains the +read plus the differences in claim protocol, slot metadata and retry behaviour, and those terms are +not ordered -- in the **isolated** regime `reserving_mpsc` is several times faster despite doing the +extra read (55.5 against 207.2 ns/push at sixteen producers in one run), so the other terms can be +large and negative. A difference that can go either way bounds the read in neither direction, and in +the drained regime which shape leads varies between runs on this host, so even its sign is not a +finding. Isolating the read would need a matched control this probe does not have. **The checklists carrying those decisions are not in this repository yet** -- they arrive with the rest of the queue work -- so this note deliberately names the QUESTIONS rather than linking to items @@ -685,7 +691,8 @@ evidence about the claim on its own. **Drained** runs a consumer popping continuously, which is the regime in which `reserving_mpsc`'s read of `head` is most expensive -- that read is cheap until a consumer is *writing* the line, and measuring it in isolation would report it as free. -It bounds that read rather than pricing it, because the ratio is between two complete push paths. +It neither isolates that read nor bounds it: the ratio is between two complete push paths whose other +differences are not ordered. The drained regime has a **single** consumer, because that is what MPSC means, so at high producer counts it becomes consumer-bound and a plateau there says nothing about the claim. Each row carries the refusal @@ -851,8 +858,11 @@ Seven runs, median of the per-run ratios with the observed range beside it, release build. **The sampling parameters are capture parameters too**: each run is a whole probe invocation, within which every configuration is measured five times and the median reported, each measurement being 50,000 pushes per producer -thread, preceded by one untimed pass that exists to fault in the fresh -allocation's pages. So a figure below rests on 35 timed passes per +thread, preceded by one untimed pass. That pass does **not** pre-touch any +allocation a timed pass will use -- every repetition builds and drops its own +queue -- so what it warms is process state: the allocator's size class, the OS +page cache, the instruction cache and the branch predictors. So a figure below +rests on 35 timed passes per configuration, and "seven runs" alone would not let anyone reproduce it. These are fixed at [src/queue_contention.rs](src/queue_contention.rs)`::PUSHES_PER_PRODUCER` and diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index ffa404d09..2a9bf9ca7 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -11,7 +11,7 @@ //! and `reserving_mpsc` should merge. See `queue_contention`'s module docs. use windows_platform_probes::queue_contention::{ - PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, Run, measure, shapes, + DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, Run, measure, shapes, }; use windows_platform_probes::report::emit_report; @@ -86,7 +86,7 @@ fn render(out: &mut dyn std::fmt::Write) { let _ = writeln!( out, - "\n-- drained: a consumer popping continuously, capacity 1024 --" + "\n-- drained: a consumer popping continuously, capacity {DRAINED_CAPACITY} --" ); render_table(out, &observation.drained); @@ -155,11 +155,11 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " regime is where that load is at its most expensive, so the ratio" + " regime is where that load is at its most expensive -- but the" ); let _ = writeln!( out, - " bounds its contribution from above rather than isolating it.\n" + " ratio still does not isolate it, or bound it either way.\n" ); let _ = writeln!( out, @@ -199,15 +199,31 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " -- which is why the ratio bounds its cost from above. It does not" + " -- but the ratio does not decompose. It is an END-TO-END" ); let _ = writeln!( out, - " price it: the two shapes also differ in claim protocol and slot" + " comparison of two shapes: they also differ in claim protocol," ); let _ = writeln!( out, - " metadata, and all of that is inside the same number." + " slot metadata and retry behaviour, and those differences are not" + ); + let _ = writeln!( + out, + " ordered. The isolated table above shows how far: reserving_mpsc" + ); + let _ = writeln!( + out, + " is several times FASTER there despite doing the extra read, so" + ); + let _ = writeln!( + out, + " the other terms can be large and negative. A difference that can" + ); + let _ = writeln!( + out, + " go either way bounds the read in neither direction." ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index 3bad385c4..cd99d0878 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -134,7 +134,7 @@ //! | [`doorbell_cost::measure`] | binary only | the absolute cost of `SetEvent`, a set/reset cycle and a satisfied wait against an uncontended atomic, and how much batching drives the doorbell below the push it accompanies | //! | [`doorbell_cost::measure_park_and_wake`] | asserted | that the park-and-wake handshake completes rather than deadlocking, which its first implementation did | //! | [`request_cost::measure`] | binary only | the absolute cost of preparing a path, building an owned `OpenFile`, and duplicating a handle | -//! | [`queue_contention::measure`] | binary only | what the bounded array queue's contended tail claim costs against a plain `fetch_add`, an upper bound on what `reserving_mpsc`'s read of the consumer's position adds (the ratio is between two complete push paths, so it bounds that read rather than isolating it), and how the claim word's bit apportionments compare -- binary only because it wants more cores than a hosted runner has, takes about a minute, and reports the two shapes as equivalent unless built with `--release` | +//! | [`queue_contention::measure`] | binary only | what the bounded array queue's contended tail claim costs against a plain `fetch_add`, how `reserving_mpsc` and `slotwise_mpsc` compare end to end in the regime where `reserving_mpsc`'s read of the consumer's position is most expensive (an end-to-end shape comparison -- it neither isolates that read nor bounds it, since the shapes differ in claim protocol and slot metadata too and those differences are not ordered), and how the claim word's bit apportionments compare -- binary only because it wants more cores than a hosted runner has, takes about a minute, and reports the two shapes as equivalent unless built with `--release` | #![cfg(windows)] #![forbid(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 586c58b7f..93987e9f4 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -46,12 +46,20 @@ //! *writing* it. Measured in isolation that read hits a clean, shared line and //! looks free -- which would be a confident wrong answer. //! -//! **It does not isolate that read either**, for the same reason the isolated -//! regime does not isolate the claim: the ratio is between two complete push -//! paths, and `reserving_mpsc` and `slotwise_mpsc` differ in claim protocol and -//! slot metadata as well as in that one load. So the ratio **bounds the read's -//! contribution from above** rather than pricing it. Found by a review, which -//! is also how the isolated bullet above got its correction. +//! **It does not isolate that read, and it does not bound it either** -- an +//! earlier correction here claimed a bound, which is no better than the +//! over-claim it replaced. The ratio is between two complete push paths, and +//! `reserving_mpsc` and `slotwise_mpsc` differ in claim protocol, slot metadata +//! and retry behaviour as well as in that one load. Writing `R` and `S` for the +//! two totals, `R - S` is the read plus those other differences, and **those +//! terms are not ordered**: in the isolated regime `reserving_mpsc` is several +//! times *faster* despite doing the extra read, so the other terms can be large +//! and negative. A difference that can go either way bounds the read in neither +//! direction -- and which shape is ahead in the drained regime varies between +//! runs on one host, so even the sign is not a finding. Read these rows as an +//! end-to-end comparison of two shapes in the regime where the read is most +//! expensive, and nothing finer. Found by a review -- the second one to correct +//! this sentence. //! //! # What is deliberately not claimed //! @@ -510,7 +518,7 @@ fn time_isolated_permit(producers: usize) -> Repetition { /// A capacity a real system would choose, so the drained regime exercises /// backpressure the way a real one would. -const DRAINED_CAPACITY: usize = 1024; +pub const DRAINED_CAPACITY: usize = 1024; fn time_drained_mpsc(producers: usize) -> Repetition { let (tx, rx) = slotwise_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); diff --git a/crates/windows-waitable-queues/Cargo.toml b/crates/windows-waitable-queues/Cargo.toml index a0cd190b8..e65db2b3d 100644 --- a/crates/windows-waitable-queues/Cargo.toml +++ b/crates/windows-waitable-queues/Cargo.toml @@ -59,8 +59,9 @@ experimental-permit-claim = [] # # Most callers should not need this. `Perpetual` reaches roughly twenty years # before its claim position recurs, on a plain `AtomicU64`; what that costs in -# throughput is not established, and the 128-bit exchange measured roughly 1.1x -# to 3.8x slower on the claim itself depending on producer count. See +# throughput is not established, and the 128-bit exchange measured +# 1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to 3.8x at sixteen +# and thirty-two. See # `ClaimLayout` for the comparison. dwcas = ["dep:portable-atomic"] diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 9c98bed7e..0cc6f9726 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` up to eight producers and sit near 1.26x at sixteen and thirty-two, against a same-code control reaching 1.12x -- which is one host declining to call it, not a cost. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and it measured roughly 1.1x at one or two producers rising to about 3.8x at thirty-two in the isolated regime, while falling inside the same-code control when drained; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` up to eight producers and sit near 1.26x at sixteen and thirty-two, against a same-code control reaching 1.12x -- which is one host declining to call it, not a cost. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and it measured 1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to 3.8x at sixteen and thirty-two in the isolated regime, while falling inside the same-code control when drained; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 63c60c04c..4f5add136 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -146,8 +146,8 @@ one to be slower -- but **what that costs in throughput is not established**: a probe comparing them found them indistinguishable up to eight producers and near 1.26x at sixteen and thirty-two, on one host, against a same-code control that itself reached 1.12x. `Wide` is a separate matter: it needs a 128-bit exchange, -which measured roughly 1.1x at one or two producers rising to about 3.8x at -thirty-two in isolation, and it is the only thing in +which measured 1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to +3.8x at sixteen and thirty-two in isolation, and it is the only thing in this crate that costs a third-party dependency. The default remains `Balanced` so that no existing caller's behaviour changed @@ -214,7 +214,8 @@ stops at 64 bits -- so the double-width compare-and-swap comes from `portable-atomic`. Most callers do not need it; `Perpetual` reaches roughly twenty years before its claim position recurs with no dependency, though what that costs in throughput is not established, while the 128-bit exchange measured -roughly 1.1x to 3.8x slower on the claim itself depending on producer count. Take +1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to 3.8x at sixteen +and thirty-two. Take it when you want the recurrence gone as a guarantee rather than deferred by an argument about deployment lifetimes. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 635d8a5b3..217c39e0f 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -115,8 +115,8 @@ //! eight producers and near 1.26x at sixteen and thirty-two, on one host, //! against a same-code control that itself reached 1.12x. `Wide` is a separate //! matter: it needs a 128-bit exchange, which -//! measured roughly 1.1x at one or two producers rising to about 3.8x at -//! thirty-two in isolation, and it is the only thing in +//! measured 1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to 3.8x +//! at sixteen and thirty-two in isolation, and it is the only thing in //! this crate //! that costs a third-party dependency. Prefer `Perpetual` unless you want the //! guarantee rather than the twenty years. diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index efcbec37f..aa1e4d3ad 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -529,10 +529,11 @@ impl ClaimLayout for Perpetual { /// but not at all. /// /// **Read the cost before choosing it.** The 128-bit exchange measured slower -/// than a `u64` one on the claim itself -- about 1.1x at one or two producers -/// rising to roughly 3.8x at thirty-two on one x86-64 host, so the penalty grows -/// with producer count; against a draining consumer the difference fell inside -/// that host's same-code control and could not be called at all. +/// than a `u64` one on the claim itself -- 1.1x to 1.4x up to four producers, +/// 1.8x at eight, and 3.5x to 3.8x at sixteen and thirty-two on one x86-64 +/// host, so the penalty grows with producer count; against a draining consumer +/// the difference fell inside that host's same-code control and could not be +/// called at all. /// [`Perpetual`] reaches about twenty years on a plain `AtomicU64`, and **what /// that costs in throughput is not established** -- see [`ClaimLayout`]. So this /// is worth taking when a guarantee is wanted in place of an argument about From 236c8d0e3d0694e63535acb024e085cbfe2addde Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 20:35:48 -0700 Subject: [PATCH 016/139] docs: stop restating volatile figures outside the one table that owns them The commentary about which shape is faster, and by what percentage, was doing no work in most of the places it appeared. **In the decomposition argument it was not relevant at all.** The point being made is that the reserving/slotwise ratio does not decompose -- and that follows purely from the shapes differing in claim protocol, slot metadata and retry behaviour as well as in the one load. It is true whichever shape is faster. The performance data was rhetorical reinforcement for a structural argument that stands on its own, and it had already gone wrong once: the previous commit asserted "reserving_mpsc is the FASTER shape here" inside the drained section, which a run contradicted because the drained ordering is not stable. Struck from the renderer, the module doc, and the design note; what remains is that the other terms are not ordered, so the ratio constrains the read in neither direction. **In the layout guidance the magnitude is relevant, but restating the table is not.** A caller choosing `Wide` needs to know the cost grows with producer count and can be several times at high counts; they do not need four copies of a per-producer-count table, each of which can drift from the measurement and from each other. The probe's `DESIGN-NOTES.md` holds the figures; `README.md`, `src/lib.rs`, `src/reserving_mpsc.rs`, `Cargo.toml` and `D-41` now describe the shape of the answer and point at it. Same for the apportionment claim: "not established" is the guidance, and it is carried by saying the difference did not clearly exceed the run-to-run variation of the same code measured twice -- not by repeating 1.26x against 1.12x in four documents. `D-41` keeps the 2-6% against 7-61% contrast, which is not a performance figure but the reason the earlier claim was withdrawn. This is the restatement-drift rule applied to numbers rather than to prose: every copy of a measured figure is a copy that can rot, and this branch has spent several review rounds proving it. Verified: fmt and clippy clean; 13 doctests including the compiled README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 7 ++---- .../src/bin/queue_contention.rs | 14 +----------- .../src/queue_contention.rs | 12 ++++------ crates/windows-waitable-queues/Cargo.toml | 5 ++--- .../windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 20 ++++++++--------- crates/windows-waitable-queues/src/lib.rs | 21 +++++++++--------- .../src/reserving_mpsc.rs | 22 ++++++++++--------- 8 files changed, 42 insertions(+), 61 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 50afa4998..b41d92326 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -631,11 +631,8 @@ to end in the regime where `reserving_mpsc`'s extra read of the consumer's posit were both wrong: the first said the probe *prices* that read, the second said it *bounds* it from above. Neither holds. Writing `R` and `S` for the two shapes' total push costs, `R - S` contains the read plus the differences in claim protocol, slot metadata and retry behaviour, and those terms are -not ordered -- in the **isolated** regime `reserving_mpsc` is several times faster despite doing the -extra read (55.5 against 207.2 ns/push at sixteen producers in one run), so the other terms can be -large and negative. A difference that can go either way bounds the read in neither direction, and in -the drained regime which shape leads varies between runs on this host, so even its sign is not a -finding. Isolating the read would need a matched control this probe does not have. +not ordered -- so the difference constrains the read in neither direction. Isolating it would need a +matched control this probe does not have. **The checklists carrying those decisions are not in this repository yet** -- they arrive with the rest of the queue work -- so this note deliberately names the QUESTIONS rather than linking to items diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 2a9bf9ca7..7c7699a3f 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -211,19 +211,7 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " ordered. The isolated table above shows how far: reserving_mpsc" - ); - let _ = writeln!( - out, - " is several times FASTER there despite doing the extra read, so" - ); - let _ = writeln!( - out, - " the other terms can be large and negative. A difference that can" - ); - let _ = writeln!( - out, - " go either way bounds the read in neither direction." + " ordered. So this ratio neither isolates the read nor bounds it." ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 93987e9f4..452a1af21 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -52,14 +52,10 @@ //! `reserving_mpsc` and `slotwise_mpsc` differ in claim protocol, slot metadata //! and retry behaviour as well as in that one load. Writing `R` and `S` for the //! two totals, `R - S` is the read plus those other differences, and **those -//! terms are not ordered**: in the isolated regime `reserving_mpsc` is several -//! times *faster* despite doing the extra read, so the other terms can be large -//! and negative. A difference that can go either way bounds the read in neither -//! direction -- and which shape is ahead in the drained regime varies between -//! runs on one host, so even the sign is not a finding. Read these rows as an -//! end-to-end comparison of two shapes in the regime where the read is most -//! expensive, and nothing finer. Found by a review -- the second one to correct -//! this sentence. +//! terms are not ordered** -- so the difference constrains the read in neither +//! direction. Read these rows as an end-to-end comparison of two shapes in the +//! regime where the read is most expensive, and nothing finer. Found by a +//! review -- the second one to correct this sentence. //! //! # What is deliberately not claimed //! diff --git a/crates/windows-waitable-queues/Cargo.toml b/crates/windows-waitable-queues/Cargo.toml index e65db2b3d..4055585a5 100644 --- a/crates/windows-waitable-queues/Cargo.toml +++ b/crates/windows-waitable-queues/Cargo.toml @@ -59,9 +59,8 @@ experimental-permit-claim = [] # # Most callers should not need this. `Perpetual` reaches roughly twenty years # before its claim position recurs, on a plain `AtomicU64`; what that costs in -# throughput is not established, and the 128-bit exchange measured -# 1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to 3.8x at sixteen -# and thirty-two. See +# throughput is not established, and the 128-bit exchange's cost grows +# with producer count -- near parity at one or two, several times by thirty-two. See # `ClaimLayout` for the comparison. dwcas = ["dep:portable-atomic"] diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 0cc6f9726..2ce33a30e 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` up to eight producers and sit near 1.26x at sixteen and thirty-two, against a same-code control reaching 1.12x -- which is one host declining to call it, not a cost. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and it measured 1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to 3.8x at sixteen and thirty-two in the isolated regime, while falling inside the same-code control when drained; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and its cost grows with producer count in the isolated regime -- near parity at one or two, several times by thirty-two, while falling inside the same-code control when drained; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 4f5add136..42f2d0821 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -143,11 +143,11 @@ let (tx, rx) = reserving_mpsc::bounded_as::(64)?; `Enduring`, and `Perpetual` all issue the same exchange on the same 64-bit word and differ only in shift and mask constants, so there is no structural reason for one to be slower -- but **what that costs in throughput is not established**: a -probe comparing them found them indistinguishable up to eight producers and near -1.26x at sixteen and thirty-two, on one host, against a same-code control that -itself reached 1.12x. `Wide` is a separate matter: it needs a 128-bit exchange, -which measured 1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to -3.8x at sixteen and thirty-two in isolation, and it is the only thing in +probe comparing them found them indistinguishable at low producer counts, and at +high counts a difference that did not clearly exceed the run-to-run variation of +the same code measured twice. `Wide` is a separate matter: it needs a 128-bit exchange, +whose cost grows with producer count -- near parity at one or two, several times +by thirty-two, in isolation -- and it is the only thing in this crate that costs a third-party dependency. The default remains `Balanced` so that no existing caller's behaviour changed @@ -184,9 +184,9 @@ proportionally longer to reach its wrap. which takes it past any real deployment. This is the answer for almost every caller who is exposed at all. **What it costs in throughput is not established** -- it issues the same `lock cmpxchg` on the same `u64` as the - default, and measured indistinguishable from it up to eight producers, but a - seven-run measurement on a single host put it near 1.26x at sixteen and - thirty-two producers against a same-code control that itself reached 1.12x. + default, and measured indistinguishable from it at low producer counts; at + high counts the difference did not clearly exceed the run-to-run variation of + the same code measured twice. Measure on your own target if throughput at high producer counts matters. - **`slotwise_mpsc` does not have this hazard** under any layout. Its positions are 64 bits on every target, so the equivalent wrap needs 2^64 claims. Prefer @@ -214,8 +214,8 @@ stops at 64 bits -- so the double-width compare-and-swap comes from `portable-atomic`. Most callers do not need it; `Perpetual` reaches roughly twenty years before its claim position recurs with no dependency, though what that costs in throughput is not established, while the 128-bit exchange measured -1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to 3.8x at sixteen -and thirty-two. Take +a cost that grows with producer count -- near parity at one or two, +several times by thirty-two. Take it when you want the recurrence gone as a guarantee rather than deferred by an argument about deployment lifetimes. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 217c39e0f..57f5c8047 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -111,12 +111,12 @@ //! `Enduring`, and `Perpetual` all issue the same exchange on the same 64-bit //! word and differ only in shift and mask constants, so there is no structural //! reason for one to be slower -- but **what that costs in throughput is not -//! established**: a probe comparing them found them indistinguishable up to -//! eight producers and near 1.26x at sixteen and thirty-two, on one host, -//! against a same-code control that itself reached 1.12x. `Wide` is a separate -//! matter: it needs a 128-bit exchange, which -//! measured 1.1x to 1.4x up to four producers, 1.8x at eight, and 3.5x to 3.8x -//! at sixteen and thirty-two in isolation, and it is the only thing in +//! established**: a probe comparing them found them indistinguishable at low +//! producer counts, and at high counts a difference that did not clearly exceed +//! the run-to-run variation of the same code measured twice. `Wide` is a separate +//! matter: it needs a 128-bit exchange, whose cost +//! grows with producer count -- near parity at one or two, several times by +//! thirty-two, in isolation -- and it is the only thing in //! this crate //! that costs a third-party dependency. Prefer `Perpetual` unless you want the //! guarantee rather than the twenty years. @@ -156,11 +156,10 @@ //! which takes it past any real deployment. This is the answer for almost //! every caller who is exposed at all. **What it costs in throughput is not //! established** -- it issues the same `lock cmpxchg` on the same `u64` as -//! the default, and measured indistinguishable from it up to eight producers, -//! but a seven-run measurement on a single host put it near 1.26x at sixteen -//! and thirty-two producers against a same-code control that itself reached -//! 1.12x. Measure on your own target if throughput at high producer counts -//! matters. +//! the default, and measured indistinguishable from it at low producer +//! counts; at high counts the difference did not clearly exceed the +//! run-to-run variation of the same code measured twice. Measure on your own +//! target if throughput at high producer counts matters. //! - **[`slotwise_mpsc`] does not have this hazard** under any layout. Its //! positions are 64 bits on every target, so the equivalent wrap needs 2^64 //! claims. Prefer it unless you need [`Reserving`]. diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index aa1e4d3ad..8d1ee2d7c 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -188,11 +188,11 @@ use crate::options::Options; /// three issue the same `lock cmpxchg` on the same `u64` and differ only in /// shift and mask constants, so there is no structural reason for one to be /// slower. **What that costs in throughput is not established**: a probe -/// comparing them found them indistinguishable up to eight producers and near -/// 1.26x at sixteen and thirty-two, on one host, against a same-code control -/// that itself reached 1.12x. Measure on your target if throughput at high -/// producer counts matters. The trade is otherwise entirely against the -/// reservation ceiling. +/// comparing them found them indistinguishable at low producer counts, and at +/// high counts a difference that did not clearly exceed the run-to-run +/// variation of the same code measured twice. Measure on your target if +/// throughput at high producer counts matters. The trade is otherwise entirely +/// against the reservation ceiling. /// /// This trait is sealed: the layouts are a fixed set because each one's /// constants are checked against each other at compile time, and a caller @@ -529,11 +529,13 @@ impl ClaimLayout for Perpetual { /// but not at all. /// /// **Read the cost before choosing it.** The 128-bit exchange measured slower -/// than a `u64` one on the claim itself -- 1.1x to 1.4x up to four producers, -/// 1.8x at eight, and 3.5x to 3.8x at sixteen and thirty-two on one x86-64 -/// host, so the penalty grows with producer count; against a draining consumer -/// the difference fell inside that host's same-code control and could not be -/// called at all. +/// than a `u64` one on the claim itself, and the penalty **grows with producer +/// count** -- near parity at one or two, several times by thirty-two, on one +/// x86-64 host; against a draining consumer the difference fell inside that +/// host's same-code control and could not be called at all. The per-count table +/// is in the queue-contention section of +/// [DESIGN-NOTES.md](../../windows-platform-probes/DESIGN-NOTES.md), which is +/// the one place it is recorded. /// [`Perpetual`] reaches about twenty years on a plain `AtomicU64`, and **what /// that costs in throughput is not established** -- see [`ClaimLayout`]. So this /// is worth taking when a guarantee is wanted in place of an argument about From 1a42d9ab51dae69ec632e5deadcdbaf5ed02fb0e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 20:42:06 -0700 Subject: [PATCH 017/139] docs(platform-probes): an observation is reviewed against its procedure, not argued Records the triage rule this branch needed and did not have. Three review rounds were spent renegotiating the wording of ratios -- prices/bounds/compares, 1.1x against 1.37x -- as though each were a position to defend. It is not a good use of review, and it re-introduces the authority D-observations-not-verdicts exists to remove: once a figure is published as a parameterised observation rather than a verdict, there is no verdict left to disagree with. The reviewable questions are procedural: right instrument, capture parameters recorded, dispersion reported, conclusion inside what the procedure supports. If those hold, the figure stands -- including when a reader would have characterised it differently. The exception, and it is the one that matters, is a statement the procedure cannot support at all: an attribution to a cause the measurement does not isolate, a claim of significance a control contradicts, or a figure restated where it can drift from its source. Those are procedural defects and get fixed. The two look alike in a review comment, which is why the distinction is written down. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index b41d92326..b08278b83 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -69,6 +69,29 @@ The failure this prevents is a reader inheriting a number as though it were a property of the code. It is a property of the code **on that machine**, and the distinction is the whole value of shipping the probe rather than only its output. +**A figure here is the output of a procedure, and that is what review checks.** +An observation is not an opinion, a position, or a point of view, so it is not +something to be argued for or against. The reviewable questions are procedural: +was the right instrument used, were the capture parameters recorded, was the +dispersion reported, does the stated conclusion stay inside what the procedure can +support. If the answer to those is yes, the figure stands as an observation -- +including when a reader would have characterised it differently. + +**So do not re-litigate a number or its wording.** A challenge of the form "this +should be called 1.4x rather than 1.1x", or "this ought to be phrased as a bound +rather than a comparison", is answered by pointing at the procedure and the table, +not by renegotiating the prose. The exception is a statement that the procedure +cannot support at all -- an attribution to a cause the measurement does not +isolate, a claim of significance a control contradicts, a figure restated +somewhere it can drift from its source. Those are procedural defects and are +fixed. The distinction is worth holding because the two look alike in a review +comment and only one is worth the time. + +This is a consequence of the decision above rather than a separate rule: once +figures are published as parameterised observations rather than as verdicts, there +is no verdict left to disagree with, and treating each characterisation as a +position to defend re-introduces exactly the authority the decision removes. + ### High variance in our own control is a finding about the instrument, not just a wider yardstick From 906aa01da35bb432f391958c323ce99d49bcffde Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 21:35:42 -0700 Subject: [PATCH 018/139] fix(platform-probes): label the instrument as what it measures, not what it is about Every finding in this round triages as procedural under the rule recorded in 1a42d9a -- a statement the procedure cannot support -- rather than as wording to renegotiate. Three classes. **Attribution to a cause the measurement does not isolate (nine sites).** The probe times each shape's WHOLE push path. The reserving/slotwise ratio was corrected for this twice already, but the same defect sat untouched in the labels around it: the module title asked whether "the tail claim" contends, the isolated table was headed "tail-claim contention", the layout paragraph said 64/64 vs 32/32 "prices the double-width exchange", and four sites in `windows-waitable-queues` attributed the producer-count curve to the 128-bit exchange itself. All now describe push-path scaling and the layout's effect on that path, with the isolated regime named where the result is regime-specific. **A mislabelled instrument.** `time_contended_atomic` starts N workers on ONE `AtomicU64`, and the report calls it the contended floor -- but the field's doc comment called it "the uncontended-atomic floor". That is the baseline every contention curve is read against, so the label mattered. **A claim the control contradicts.** The report told readers the gap between the `reserving_mpsc` and `32/32` rows -- same code, measured twice -- "is this host's zero". The measured control spans 0.68-1.27x across seven runs, which is the opposite of zero and wide enough to swallow the layout differences. Since this line exists to point AT the control, it said precisely the wrong thing. Also: - `capacity_for_...`'s assertion used `>` where the contract needs `>=`. Settled by measuring rather than reading: `bounded(8)` accepts exactly 8 items, so an exact fit is sufficient. Unreachable today (the product is never a power of two) but M4.2 makes the push count settable, and a test stricter than the contract would reject a valid configuration then. - `reserving_mpsc`'s ceiling sentence said the trade was "otherwise entirely against the reservation ceiling" immediately after saying throughput is not established. Now: the ceiling is the settled trade, throughput remains target-dependent. - `D-26` and `D-35` in the queue crate rest on "three invocations agreed within noise", read against the 2-6% floor this branch replaced with a measured 7-61%. The figures are not retracted -- D-26's direction survived re-measurement -- but both sites now say that "within noise" is a weaker statement than it reads as, and link to the variance decision. This is the re-check that a corrected rule obliges, not a re-measurement. Verified: fmt and clippy clean; 16 queue_contention tests; 13 doctests including the compiled README; cross-crate anchor resolves. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 24 +++++++++++++++---- crates/windows-platform-probes/src/lib.rs | 2 +- .../src/queue_contention.rs | 14 +++++++++-- .../src/queue_contention/tests.rs | 10 ++++++-- crates/windows-waitable-queues/Cargo.toml | 6 +++-- .../windows-waitable-queues/DESIGN-NOTES.md | 22 +++++++++++++++-- crates/windows-waitable-queues/README.md | 11 +++++---- crates/windows-waitable-queues/src/lib.rs | 7 +++--- .../src/reserving_mpsc.rs | 18 +++++++------- 9 files changed, 84 insertions(+), 30 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 7c7699a3f..4609e9da1 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -93,7 +93,10 @@ fn render(out: &mut dyn std::fmt::Write) { let _ = writeln!(out, "\ninterpretation:\n"); // Question 1: does the claim collapse as producers are added? - let _ = writeln!(out, " 1. tail-claim contention (isolated regime)\n"); + let _ = writeln!( + out, + " 1. push-path scaling with producer count (isolated regime)\n" + ); let _ = writeln!( out, " {:<18} {:>12} {:>12} {:>12} {:>14}", @@ -282,16 +285,27 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " the 32/32 row above are the same code, so their gap is this" + " the 32/32 row above are the same code, so the gap between them is" + ); + let _ = writeln!( + out, + " what 'no difference' looks like on this host -- which across seven" + ); + let _ = writeln!( + out, + " runs was not zero, and was wide enough to swallow the layout rows." + ); + let _ = writeln!( + out, + " 64/64 vs 32/32 is the double-width layout's effect on the whole" ); - let _ = writeln!(out, " host's zero."); let _ = writeln!( out, - " 64/64 vs 32/32 prices the double-width exchange -- what removing" + " push path -- what removing the recurrence outright costs, against" ); let _ = writeln!( out, - " the recurrence outright costs, against 8/56 merely deferring it.\n" + " 8/56 merely deferring it. Not the exchange in isolation.\n" ); for (label, regime) in [ ("isolated", &observation.isolated), diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index cd99d0878..5e74ed70f 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -134,7 +134,7 @@ //! | [`doorbell_cost::measure`] | binary only | the absolute cost of `SetEvent`, a set/reset cycle and a satisfied wait against an uncontended atomic, and how much batching drives the doorbell below the push it accompanies | //! | [`doorbell_cost::measure_park_and_wake`] | asserted | that the park-and-wake handshake completes rather than deadlocking, which its first implementation did | //! | [`request_cost::measure`] | binary only | the absolute cost of preparing a path, building an owned `OpenFile`, and duplicating a handle | -//! | [`queue_contention::measure`] | binary only | what the bounded array queue's contended tail claim costs against a plain `fetch_add`, how `reserving_mpsc` and `slotwise_mpsc` compare end to end in the regime where `reserving_mpsc`'s read of the consumer's position is most expensive (an end-to-end shape comparison -- it neither isolates that read nor bounds it, since the shapes differ in claim protocol and slot metadata too and those differences are not ordered), and how the claim word's bit apportionments compare -- binary only because it wants more cores than a hosted runner has, takes about a minute, and reports the two shapes as equivalent unless built with `--release` | +//! | [`queue_contention::measure`] | binary only | how the bounded array queue's whole push path scales with producer count, against a contended `fetch_add` floor -- the tail claim is one term in that path, not the whole of it; how `reserving_mpsc` and `slotwise_mpsc` compare end to end in the regime where `reserving_mpsc`'s read of the consumer's position is most expensive (an end-to-end shape comparison -- it neither isolates that read nor bounds it, since the shapes differ in claim protocol and slot metadata too and those differences are not ordered); and each claim-word apportionment's effect on that same whole path -- binary only because it wants more cores than a hosted runner has, takes about a minute, and reports the two shapes as equivalent unless built with `--release` | #![cfg(windows)] #![forbid(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 452a1af21..f2932e041 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -1,6 +1,11 @@ // Copyright (c) Mike Grier. -//! Does the array queue's tail claim contend at realistic producer counts? +//! How does the array queue's push path scale with producer count? +//! +//! The question behind it is whether the **tail claim** contends badly enough to +//! justify other MPSC shapes -- but what is timed is each shape's whole push +//! path, so the curve is push-path scaling and the claim is one term in it. See +//! the regime notes below before attributing any difference to the claim. //! //! **An experiment, not a component.** These probes measure platform behaviour //! and are not for production use. Do not call them from production code, and @@ -125,7 +130,12 @@ pub mod shapes { /// The experimental permit-claiming MPSC, measured against /// [`RESERVING_MPSC`] because it is a candidate replacement for it. pub const PERMIT_MPSC: &str = "permit_mpsc"; - /// The uncontended-atomic floor the queues are measured against. + /// The contended-atomic floor the queues are measured against. + /// + /// Contended, not uncontended: every producer thread increments the **same** + /// `AtomicU64`, which is the point -- it is the cheapest possible thing N + /// threads can do to one cache line, so it separates what the queue costs + /// from what this processor does to a fought-over line. pub const BASELINE_FETCH_ADD: &str = "baseline_fetch_add"; /// `reserving_mpsc` on its default layout: a `u64` split 32 / 32. /// diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 053032a2e..ce7268ffc 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -250,13 +250,19 @@ fn every_shape_name_is_distinct() { /// `capacity_for` must leave room for every push, or the isolated regime would /// refuse and stop being the regime it claims to be. +/// +/// `>=` rather than `>`: a `bounded(n)` queue accepts exactly `n` items +/// (measured, not assumed), so an exact fit is sufficient. Today the product is +/// never a power of two, so the distinction is unreachable -- but M4.2 makes the +/// push count settable, and a stricter assertion than the contract requires would +/// reject a valid configuration then. #[test] -fn capacity_for_exceeds_the_pushes_every_producer_count_will_make() { +fn capacity_for_leaves_room_for_every_push_at_every_producer_count() { for &producers in PRODUCER_COUNTS { let capacity = capacity_for(producers); let pushes = producers * PUSHES_PER_PRODUCER; assert!( - capacity > pushes, + capacity >= pushes, "{producers} producers push {pushes} but capacity is {capacity}" ); assert!( diff --git a/crates/windows-waitable-queues/Cargo.toml b/crates/windows-waitable-queues/Cargo.toml index 4055585a5..a6850621b 100644 --- a/crates/windows-waitable-queues/Cargo.toml +++ b/crates/windows-waitable-queues/Cargo.toml @@ -59,8 +59,10 @@ experimental-permit-claim = [] # # Most callers should not need this. `Perpetual` reaches roughly twenty years # before its claim position recurs, on a plain `AtomicU64`; what that costs in -# throughput is not established, and the 128-bit exchange's cost grows -# with producer count -- near parity at one or two, several times by thirty-two. See +# throughput is not established, and choosing it measured slower on the +# whole push path as producer count rises -- near parity at one or two, several +# times by thirty-two, in the isolated regime. The probe times the complete push, +# so that is the layout's effect on that path, not the exchange in isolation. See # `ClaimLayout` for the comparison. dwcas = ["dep:portable-atomic"] diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 2ce33a30e..b2186ee50 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and its cost grows with producer count in the isolated regime -- near parity at one or two, several times by thirty-two, while falling inside the same-code control when drained; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and choosing it measured slower on the whole push path as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | ## D-2: capabilities are sliced, not gathered @@ -844,7 +844,19 @@ refused; a consumer wants to know how deep the backlog got and how often it was Measured by `probe-queue-contention` in a **release** build on an AMD EPYC 7763, 8 cores / 16 logical processors, Windows 11 Enterprise 10.0.26200, `x86_64`. Median of five repetitions after a discarded -warm-up; three independent invocations agreed to within noise. **Note the architecture**: every previous +warm-up; three independent invocations agreed to within noise. + +**That "within noise" rests on a floor this workspace has since measured to be far +wider.** The figure was read against a 2-6% run-to-run spread; seven runs of the +same probe later put the same-configuration spread at 7-61% depending on producer +count, and the probe's own same-code control spans 0.68-1.27x. The figures below +are not retracted -- the direction of `D-26` survived a re-measurement on the +shipping type -- but "agreed within noise" is a weaker statement than it reads as, +and any difference here smaller than that control should not be treated as +established. See +[DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). + +**Note the architecture**: every previous measurement in this workspace was taken on the ARM64 development machine, so these numbers fill the x64 gap rather than extending the ARM64 record, and the two are not interchangeable. @@ -1251,6 +1263,12 @@ Run by `probe-queue-contention` on the reference host (x86-64, 16 logical / 8 ph release build, five repetitions per configuration with the median kept. The whole run was repeated three times; the isolated numbers reproduced within noise except one outlier noted below. +**Read "within noise" here against the wider floor measured later**: seven runs +of this probe put the same-configuration spread at 7-61%, and its same-code +control at 0.68-1.27x, so three agreeing runs establish less than the phrase +suggests. See +[DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). + ### Isolated regime -- producers only, nothing ever refused The cleanest measurement of the claim, because nothing else touches the queue. Nanoseconds per push: diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 42f2d0821..1266fbaa7 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -146,8 +146,9 @@ one to be slower -- but **what that costs in throughput is not established**: a probe comparing them found them indistinguishable at low producer counts, and at high counts a difference that did not clearly exceed the run-to-run variation of the same code measured twice. `Wide` is a separate matter: it needs a 128-bit exchange, -whose cost grows with producer count -- near parity at one or two, several times -by thirty-two, in isolation -- and it is the only thing in +and choosing it measured slower on the whole push path as producer count rises +-- near parity at one or two, several times by thirty-two, in the isolated +regime -- and it is the only thing in this crate that costs a third-party dependency. The default remains `Balanced` so that no existing caller's behaviour changed @@ -213,9 +214,9 @@ dependency: Rust's standard library has no 128-bit atomic -- `core::sync::atomic stops at 64 bits -- so the double-width compare-and-swap comes from `portable-atomic`. Most callers do not need it; `Perpetual` reaches roughly twenty years before its claim position recurs with no dependency, though what -that costs in throughput is not established, while the 128-bit exchange measured -a cost that grows with producer count -- near parity at one or two, -several times by thirty-two. Take +that costs in throughput is not established, while choosing `Wide` measured +slower on the whole push path as producer count rises -- near parity at one or +two, several times by thirty-two, in the isolated regime. Take it when you want the recurrence gone as a guarantee rather than deferred by an argument about deployment lifetimes. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 57f5c8047..1c057fae9 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -114,9 +114,10 @@ //! established**: a probe comparing them found them indistinguishable at low //! producer counts, and at high counts a difference that did not clearly exceed //! the run-to-run variation of the same code measured twice. `Wide` is a separate -//! matter: it needs a 128-bit exchange, whose cost -//! grows with producer count -- near parity at one or two, several times by -//! thirty-two, in isolation -- and it is the only thing in +//! matter: it needs a 128-bit exchange, and choosing it measured slower on the +//! whole push path as producer count rises -- near parity at one or two, +//! several times by thirty-two, in the isolated regime -- and it is the only +//! thing in //! this crate //! that costs a third-party dependency. Prefer `Perpetual` unless you want the //! guarantee rather than the twenty years. diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 8d1ee2d7c..777af953e 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -191,8 +191,8 @@ use crate::options::Options; /// comparing them found them indistinguishable at low producer counts, and at /// high counts a difference that did not clearly exceed the run-to-run /// variation of the same code measured twice. Measure on your target if -/// throughput at high producer counts matters. The trade is otherwise entirely -/// against the reservation ceiling. +/// throughput at high producer counts matters. The one trade that IS settled is +/// the reservation ceiling; throughput remains target-dependent. /// /// This trait is sealed: the layouts are a fixed set because each one's /// constants are checked against each other at compile time, and a caller @@ -528,12 +528,14 @@ impl ClaimLayout for Perpetual { /// pushes to recur, which no deployment reaches -- not "not for twenty years", /// but not at all. /// -/// **Read the cost before choosing it.** The 128-bit exchange measured slower -/// than a `u64` one on the claim itself, and the penalty **grows with producer -/// count** -- near parity at one or two, several times by thirty-two, on one -/// x86-64 host; against a draining consumer the difference fell inside that -/// host's same-code control and could not be called at all. The per-count table -/// is in the queue-contention section of +/// **Read the cost before choosing it.** Choosing `Wide` measured slower on the +/// whole push path than a `u64` layout does, and the penalty **grows with +/// producer count** -- near parity at one or two, several times by thirty-two, +/// in the isolated regime on one x86-64 host; against a draining consumer the +/// difference fell inside that host's same-code control and could not be called +/// at all. The probe times the complete push, so this is the layout's effect on +/// that path and not a measurement of the 128-bit exchange on its own. The +/// per-count table is in the queue-contention section of /// [DESIGN-NOTES.md](../../windows-platform-probes/DESIGN-NOTES.md), which is /// the one place it is recorded. /// [`Perpetual`] reaches about twenty years on a plain `AtomicU64`, and **what From 0ad74651e34e07078e697b0d6e4d3a7892ee692f Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 22:24:25 -0700 Subject: [PATCH 019/139] docs: remove advice built on top of measurements A recommendation converts an observation into an undertaking, and it is an undertaking nobody here can honour: this crate has measured one machine, and the reader's deployment is not that machine. Telling a caller which shape or layout to pick takes on responsibility for an outcome we have no standing to hold. Struck from the queue crate's client-facing documentation: - "Otherwise, **start with `reserving_mpsc`** -- it was the faster of the two at every producer count we measured above one" and "Use `spsc`, which beats both". The README's own preceding sentence says the crate ships both "rather than picking one for you", and then the block picked one. - "Prefer `Perpetual` unless...", "Prefer it unless you need `Reserving`", "Prefer `Enduring` or `Perpetual` unless you genuinely hold more than 65,535 reservations". - "Most callers do not need it" / "Most callers should not need this". - "It is not the recommended layout", three sites -- now states the fact instead: the default carries the recurrence described above. - "which you should read before choosing". And four I added in this branch, which is what prompted the instruction: - "This is the answer for almost every caller who is exposed at all" -- a recommendation wearing a hedge. - "Measure on your own target if throughput at high producer counts matters", three sites. This sounds like caution but is still an instruction, and it implies that a reader who did not measure has made a mistake we warned them about. - "**Read the cost before choosing it.**" - "is worth taking when a guarantee is wanted in place of..." - In the probe's own note: "A client who needs the headroom should measure the layouts on their target." Nothing is withheld. The figures, capture parameters, dispersion, hazards, structural differences between the shapes, and the limits of what the instrument can separate are all still published. What is removed is the imperative mood, and the "Start here" block becomes "What distinguishes them" -- the same facts without a ranking. Records D-no-client-prescriptions so this stops recurring, including the two forms that read as caution rather than advice. Verified: fmt and clippy clean; 13 doctests including the compiled README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 37 +++++++++++--- crates/windows-waitable-queues/Cargo.toml | 2 +- crates/windows-waitable-queues/README.md | 48 +++++++++---------- crates/windows-waitable-queues/src/lib.rs | 40 ++++++++-------- .../src/reserving_mpsc.rs | 42 ++++++++-------- 5 files changed, 98 insertions(+), 71 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index b08278b83..6832e7996 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -58,12 +58,38 @@ Which position/reservation apportionment a queue should use is exactly such a choice: the layout is a type parameter of the shipping queue, the probe measures every candidate, and the note reports what it saw. It does not name a winner. +### Do not tell a reader what to do about a measurement + + + +State what was observed, under what parameters, and what the procedure could not +determine. Stop there. Advice built on top of a measurement -- "prefer this +layout", "most callers do not need that", "measure on your own target if +throughput matters", "start with this shape" -- **takes on a responsibility this +crate has no standing to hold**: it has measured one machine, and the reader's +deployment is not that machine. A recommendation converts an observation into an +undertaking, and the undertaking is one nobody here can honour. + +This is not the same as withholding information. Everything needed to decide +still gets published: the figures, their capture parameters, their dispersion, +the hazards, the structural differences between the shapes, and the limits of +what the instrument can separate. What is removed is the imperative mood. + +Two forms are easy to miss because they sound like caution rather than advice. +"Measure on your own hardware before choosing" is still an instruction, and it +implies the reader's not having done so is a mistake we warned them about. +"This is the answer for almost every caller" is a recommendation wearing a +hedge. Both are struck. A statement about what a *layout provides* is a fact and +stays; a statement about what a *reader should pick* is not. + This is why the apportionment claim in the queue-contention section was *withdrawn in both directions* rather than reversed. The measurement stopped supporting "re-apportioning is free", but it equally did not support "it costs 30%" -- one host, seven runs, against a control that wanders. The correct output -of a probe that cannot call something is a flag saying *measure this on your own -hardware*, never a verdict chosen because a verdict reads better. +of a probe that cannot call something is **the record that it could not**, never +a verdict chosen because a verdict reads better. What a reader does with that is +the reader's decision, and stating it for them would take a responsibility this +crate has no standing to hold: it has measured one machine. The failure this prevents is a reader inheriting a number as though it were a property of the code. It is a property of the code **on that machine**, and the @@ -961,10 +987,9 @@ single run against a noise floor quoted as 2-6%, and neither half holds: the measured control is far wider than 2-6%, and the re-apportionments do not sit inside it at sixteen and thirty-two producers. But the replacement is *not* the opposite claim. 1.23-1.30x against a control that itself reaches 1.12x is a -flag, not a finding -- it says this is the configuration worth measuring on your -own hardware before choosing, and it says this probe, on this host, at seven -runs, could not call it. A client who needs the headroom should measure the -layouts on their target rather than inherit either verdict from here. This is +flag, not a finding -- what it records is that this probe, on this host, at seven +runs, could not separate the layouts at high producer counts. Nothing here +establishes an ordering between them, in either direction. This is [the rule for what this crate concludes](#d-observations-not-verdicts) applied to the case that earned it. diff --git a/crates/windows-waitable-queues/Cargo.toml b/crates/windows-waitable-queues/Cargo.toml index a6850621b..ad13ceafd 100644 --- a/crates/windows-waitable-queues/Cargo.toml +++ b/crates/windows-waitable-queues/Cargo.toml @@ -57,7 +57,7 @@ experimental-permit-claim = [] # while still compiling. With them off, `AtomicU128` does not exist on such a # target and the build fails naming it. # -# Most callers should not need this. `Perpetual` reaches roughly twenty years +# `Perpetual` reaches roughly twenty years # before its claim position recurs, on a plain `AtomicU64`; what that costs in # throughput is not established, and choosing it measured slower on the # whole push path as producer count rises -- near parity at one or two, several diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 1266fbaa7..9736d8d37 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -152,7 +152,7 @@ regime -- and it is the only thing in this crate that costs a third-party dependency. The default remains `Balanced` so that no existing caller's behaviour changed -when the choice was introduced. It is not the recommended layout. +when the choice was introduced. It carries the recurrence described above. **What happens.** A producer checks that there is room, is descheduled, and resumes after other producers have driven the position field through a complete @@ -179,19 +179,17 @@ width, so they are a floor on time rather than a forecast: a queue that must drain cannot sustain the fastest rate measured, and a slower producer takes proportionally longer to reach its wrap. -**What to do about it.** +**What bears on it.** -- **Name a layout.** `Perpetual` puts the recurrence about twenty years out, - which takes it past any real deployment. This is the answer for almost every - caller who is exposed at all. **What it costs in throughput is not - established** -- it issues the same `lock cmpxchg` on the same `u64` as the +- **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty + years out. **What it costs in throughput is not established** -- it issues the + same `lock cmpxchg` on the same `u64` as the default, and measured indistinguishable from it at low producer counts; at high counts the difference did not clearly exceed the run-to-run variation of the same code measured twice. - Measure on your own target if throughput at high producer counts matters. - **`slotwise_mpsc` does not have this hazard** under any layout. Its positions - are 64 bits on every target, so the equivalent wrap needs 2^64 claims. Prefer - it unless you need `Reserving`. + are 64 bits on every target, so the equivalent wrap needs 2^64 claims. It does + not offer `Reserving`. - **`spsc` never had it**, having no contended claim to race. - **The default layout is sound below its wrap.** A queue that will not push 4.3 billion items in one run, or that is not driven at sustained maximum rate by @@ -212,7 +210,7 @@ Both are off by default, and the default build depends on `windows-sys` alone. `reserving_mpsc`. This is the only thing in the crate that costs a third-party dependency: Rust's standard library has no 128-bit atomic -- `core::sync::atomic` stops at 64 bits -- so the double-width compare-and-swap comes from -`portable-atomic`. Most callers do not need it; `Perpetual` reaches roughly +`portable-atomic`. `Perpetual` reaches roughly twenty years before its claim position recurs with no dependency, though what that costs in throughput is not established, while choosing `Wide` measured slower on the whole push path as producer count rises -- near parity at one or @@ -349,23 +347,25 @@ position, which is the only way a reservation can be answered at all. Both are well-studied designs in production use elsewhere, which is why this crate ships both rather than picking one for you. -**Start here:** +**What distinguishes them:** - **Pushing more than ~4 billion items in one run, from two or more producers?** - Either use `slotwise_mpsc`, whose positions are 64 bits under every - configuration, or name a deeper layout on `reserving_mpsc` -- `Perpetual` - puts the recurrence about twenty years out, though what it costs in throughput - is not established. Under its - default layout `reserving_mpsc` can lose an item past that volume; see + Under its default layout `reserving_mpsc` can lose an item past that volume. + `slotwise_mpsc`'s positions are 64 bits under every configuration, and naming a + deeper layout on `reserving_mpsc` moves the recurrence out -- `Perpetual` to + about twenty years -- though what that costs in throughput is not established. + The mechanism is in [the section on recurrence](#how-long-reserving_mpsc-runs-before-its-claim-position-recurs) - above, which you should read before choosing. -- Need `reserve`? Only `reserving_mpsc` has it, and `slotwise_mpsc` structurally - cannot. That no longer forces a trade against the recurrence: choosing a - layout addresses it, so the capability can settle the choice on its own - merits. -- Otherwise, **start with `reserving_mpsc`.** It was the faster of the two at - every producer count we measured above one. -- Only one producer *and* one consumer? Use `spsc`, which beats both. + above. +- **`reserve` exists only on `reserving_mpsc`**; `slotwise_mpsc` structurally + cannot offer it. That no longer forces a trade against the recurrence, since + naming a layout addresses it. +- **`spsc` requires exactly one producer and one consumer**, and does less work + than either MPSC shape because of it. + +The measurements below are what this workspace observed on the hosts named; they +are not a ranking, and which shape suits a given deployment is the deployment's +question. **What we measured**, in ns per push, isolated regime, median of three runs. Higher producer counts oversubscribe both hosts: diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 1c057fae9..b0fbfd7c1 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -119,11 +119,11 @@ //! several times by thirty-two, in the isolated regime -- and it is the only //! thing in //! this crate -//! that costs a third-party dependency. Prefer `Perpetual` unless you want the -//! guarantee rather than the twenty years. +//! that costs a third-party dependency. What it provides that `Perpetual` does +//! not is the recurrence removed outright rather than deferred. //! //! The default remains `Balanced` so that no existing caller's behaviour -//! changed when the choice was introduced. It is not the recommended layout. +//! changed when the choice was introduced. It carries the recurrence above. //! //! **What happens.** A producer checks that there is room, is descheduled, and //! resumes after other producers have driven the position field through a @@ -151,19 +151,17 @@ //! drain cannot sustain the fastest rate measured, and a slower producer takes //! proportionally longer to reach its wrap. //! -//! **What to do about it.** +//! **What bears on it.** //! -//! - **Name a layout.** `Perpetual` puts the recurrence about twenty years out, -//! which takes it past any real deployment. This is the answer for almost -//! every caller who is exposed at all. **What it costs in throughput is not +//! - **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty +//! years out. **What it costs in throughput is not //! established** -- it issues the same `lock cmpxchg` on the same `u64` as //! the default, and measured indistinguishable from it at low producer //! counts; at high counts the difference did not clearly exceed the -//! run-to-run variation of the same code measured twice. Measure on your own -//! target if throughput at high producer counts matters. +//! run-to-run variation of the same code measured twice. //! - **[`slotwise_mpsc`] does not have this hazard** under any layout. Its //! positions are 64 bits on every target, so the equivalent wrap needs 2^64 -//! claims. Prefer it unless you need [`Reserving`]. +//! claims. It does not offer [`Reserving`]. //! - **[`spsc`] never had it**, having no contended claim to race. //! - **The default layout is sound below its wrap.** A queue that will not push //! 4.3 billion items in one run, or that is not driven at sustained maximum @@ -275,15 +273,19 @@ //! which is why this crate ships both instead of picking one for you. //! //! - **Pushing more than ~4 billion items in one run, from two or more -//! producers?** Use [`slotwise_mpsc`]. [`reserving_mpsc`] has a known -//! item-loss defect past that volume, on every target -- see the section -//! above, which you should read before choosing. -//! - Need [`Reserving`]? Only [`reserving_mpsc`] has it; [`slotwise_mpsc`] structurally -//! cannot. Weigh that against the defect above rather than treating the -//! capability as settling the choice. -//! - Otherwise **start with [`reserving_mpsc`]**: it was the faster of the two -//! at every producer count above one that we measured. -//! - One producer *and* one consumer? Use [`spsc`], which beats both. +//! producers?** [`reserving_mpsc`] under its default layout has a known +//! item-loss defect past that volume, on every target; [`slotwise_mpsc`]'s +//! positions are 64 bits under every configuration, and naming a deeper layout +//! on [`reserving_mpsc`] moves the recurrence out. The mechanism is in the +//! section above. +//! - **[`Reserving`] exists only on [`reserving_mpsc`]**; [`slotwise_mpsc`] +//! structurally cannot offer it. Naming a layout addresses the recurrence, so +//! that no longer trades against this capability. +//! - **[`spsc`] requires exactly one producer and one consumer**, and does less +//! work than either MPSC shape because of it. +//! +//! The measurements below are what this workspace observed on the hosts named; +//! they are not a ranking. //! //! Measured ns per push, isolated regime, median of three. An AMD EPYC 7763 //! slice (8 cores, 16 threads) and a Snapdragon X2 Elite (12 cores, no SMT): diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 777af953e..c3e212250 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -42,7 +42,7 @@ //! ``` //! //! The default stays `Balanced` so that introducing the choice changed no -//! existing caller's behaviour; it is not the recommended layout. +//! existing caller's behaviour; it carries the recurrence described above. //! [`slotwise_mpsc`](crate::slotwise_mpsc) does not have this hazard under any //! layout, its positions being 64 bits on every target; [`spsc`](crate::spsc) //! never had it. The full statement is in the [crate documentation](crate). @@ -190,9 +190,9 @@ use crate::options::Options; /// slower. **What that costs in throughput is not established**: a probe /// comparing them found them indistinguishable at low producer counts, and at /// high counts a difference that did not clearly exceed the run-to-run -/// variation of the same code measured twice. Measure on your target if -/// throughput at high producer counts matters. The one trade that IS settled is -/// the reservation ceiling; throughput remains target-dependent. +/// variation of the same code measured twice. The settled trade is the +/// reservation ceiling; throughput is target-dependent and this crate does not +/// characterise it beyond the one host in the note above. /// /// This trait is sealed: the layouts are a fixed set because each one's /// constants are checked against each other at compile time, and a caller @@ -476,11 +476,10 @@ impl ClaimWord for u128 { /// /// Holds 2^32 outstanding reservations and recurs after 2^32 pushes -- about /// **37 seconds** of sustained maximum-rate pushing. This is the default -/// because it is what the shape shipped with, not because it is the best -/// choice: the reservation ceiling it buys is far beyond any real use, and it -/// is paid for with the whole of the `SH-14.1` exposure. Prefer [`Enduring`] or -/// [`Perpetual`] unless you genuinely hold more than 65,535 reservations at -/// once. +/// because it is what the shape shipped with: the reservation ceiling it buys +/// is far beyond any use this crate has seen, and it is paid for with the whole +/// of the `SH-14.1` exposure. [`Enduring`] and [`Perpetual`] trade ceiling for +/// recurrence in the other direction; [`Enduring`]'s ceiling is 65,535. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Balanced; impl sealed::Sealed for Balanced {} @@ -528,20 +527,21 @@ impl ClaimLayout for Perpetual { /// pushes to recur, which no deployment reaches -- not "not for twenty years", /// but not at all. /// -/// **Read the cost before choosing it.** Choosing `Wide` measured slower on the -/// whole push path than a `u64` layout does, and the penalty **grows with -/// producer count** -- near parity at one or two, several times by thirty-two, -/// in the isolated regime on one x86-64 host; against a draining consumer the -/// difference fell inside that host's same-code control and could not be called -/// at all. The probe times the complete push, so this is the layout's effect on -/// that path and not a measurement of the 128-bit exchange on its own. The -/// per-count table is in the queue-contention section of +/// The 128-bit exchange measured slower on the whole push path than a `u64` +/// layout does, and the difference **grows with producer count** -- near parity +/// at one or two, several times by thirty-two, in the isolated regime on one +/// x86-64 host; against a draining consumer the difference fell inside that +/// host's same-code control and could not be called at all. The probe times the +/// complete push, so this is the layout's effect on that path and not a +/// measurement of the 128-bit exchange on its own. The per-count table is in the +/// queue-contention section of /// [DESIGN-NOTES.md](../../windows-platform-probes/DESIGN-NOTES.md), which is /// the one place it is recorded. -/// [`Perpetual`] reaches about twenty years on a plain `AtomicU64`, and **what -/// that costs in throughput is not established** -- see [`ClaimLayout`]. So this -/// is worth taking when a guarantee is wanted in place of an argument about -/// deployment lifetimes, not because the narrow alternative is known to be free. +/// +/// [`Perpetual`] reaches about twenty years on a plain `AtomicU64`, and what +/// that costs in throughput is not established -- see [`ClaimLayout`]. What this +/// layout provides that the others do not is the recurrence removed outright +/// rather than deferred. /// /// The reservation ceiling is [`u32::MAX`] rather than the 64 bits the field /// could hold, because the count is reported to callers as a `u32`. From 9c5f3e0442a14c2d8506a1d5827a678a0d817c9e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 22:37:41 -0700 Subject: [PATCH 020/139] docs: label pre-correction figures, and state the defect instead of recommending against it **The README and public rustdoc were publishing known-optimistic numbers.** The three-run comparison table's x64 sixteen-producer row is 193.5 / 52.2 -- the exact pair the probe's note flags as predating the correction to the timing window. The sentence added in 0ad7465 then framed the table as "what this workspace observed", with no caveat, which made it worse. Both copies are now labelled: the direction survived re-measurement on the x64 host, the absolute numbers are optimistic, and the high-producer rows most so. Not retaken, because retaking needs the EPYC 7763 and Snapdragon X2 Elite named beneath the table and the correction was measured on neither. **Three sites still had the probe deciding design questions.** The binary's module doc said it "decides two things that are otherwise decided by taste"; the library module said a cheap `head` read means the shapes merge and an expensive one vindicates the split; the regime summary said to read the drained rows "for the cost of `head`". The same file says two paragraphs later that the drained ratio can neither isolate nor bound that read. All three now describe what the regimes report and say plainly that the merge question is an input to a decision rather than the decision. **On the review's request to restore "Perpetual is the practical recommendation" and "Balanced is not recommended": declined, but the concern behind it was real.** A recommendation is exactly what D-no-client-prescriptions removes. The legitimate half is that "carries the recurrence described above" was too weak to keep the hazardous default from looking endorsed by inertia -- a skimming reader does not connect "recurrence" to losing data. The answer is a stronger fact, not a restored preference: the default's documentation now says that past 2^32 pushes from two or more producers the queue can **silently lose an item**, in all four places it is described. That is more informative than "not recommended" and carries no judgement about anyone's deployment. D-41's clause is updated to match what the documentation now says, and records why the form changed. Verified: fmt and clippy clean; 13 doctests including the compiled README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 8 +++++--- .../src/queue_contention.rs | 12 ++++++++---- crates/windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 14 +++++++++++++- crates/windows-waitable-queues/src/lib.rs | 13 ++++++++++++- .../src/reserving_mpsc.rs | 18 ++++++++++++------ 6 files changed, 51 insertions(+), 16 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 4609e9da1..1b0a435d4 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -6,9 +6,11 @@ //! and are not for production use. Do not call them from production code, and //! do not lift a technique out of here. See this crate's DESIGN-NOTES.md. //! -//! This decides two things that are otherwise decided by taste: whether the -//! linked and sharded MPSC shapes are ever needed, and whether `slotwise_mpsc` -//! and `reserving_mpsc` should merge. See `queue_contention`'s module docs. +//! This reports observations that bear on two questions otherwise settled by +//! taste: whether the linked and sharded MPSC shapes are ever needed, and +//! whether `slotwise_mpsc` and `reserving_mpsc` should merge. It does not settle +//! either -- see `queue_contention`'s module docs for what the regimes can and +//! cannot separate. use windows_platform_probes::queue_contention::{ DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, Run, measure, shapes, diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index f2932e041..3dd9539ff 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -26,8 +26,11 @@ //! **2. Should `slotwise_mpsc` and `reserving_mpsc` merge?** They ship as peers because //! honouring a reservation costs the producer a read of the consumer's //! position -- one line every thread touches -- and *how much* that costs was a -//! judgement rather than a measurement. If it is cheap, the two shapes merge and -//! the non-reserving one goes; if it is expensive, the split is vindicated. +//! judgement rather than a measurement. This probe does not turn it into one: +//! the drained rows compare two complete push paths and cannot separate that +//! read from the other differences between the shapes. What they supply is an +//! end-to-end comparison in the regime where the read is most expensive, which +//! is an input to that decision rather than the decision. //! //! # Two regimes, because one of them cannot answer the second question //! @@ -69,8 +72,9 @@ //! consumer-bound, and a throughput plateau there says nothing about the tail //! claim. The probe reports each run's refusal count -- from the queue's own //! `Observable` counters -- so a backpressure-bound run is visible as a fact -//! rather than mistaken for contention. Read the isolated regime for the -//! contention question, and the drained one for the cost of `head`. +//! rather than mistaken for contention. Read the isolated regime for push-path +//! scaling with producer count, and the drained one for the end-to-end shape +//! comparison taken while `head` is being written. use std::sync::Arc; use std::sync::Barrier; diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index b2186ee50..f2bc35be3 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and choosing it measured slower on the whole push path as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and it is documented as *not* the recommended layout: leaving it because it is the status quo would preserve the hazard by inertia. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and choosing it measured slower on the whole push path as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 9736d8d37..b2333d29b 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -152,7 +152,10 @@ regime -- and it is the only thing in this crate that costs a third-party dependency. The default remains `Balanced` so that no existing caller's behaviour changed -when the choice was introduced. It carries the recurrence described above. +when the choice was introduced. Under it, a queue driven past 2^32 pushes by two +or more producers can **silently lose an item** -- the defect described above. +`Enduring` and `Perpetual` move that point out by 2^16 and 2^24 respectively, and +`Wide` removes it. **What happens.** A producer checks that there is room, is descheduled, and resumes after other producers have driven the position field through a complete @@ -367,6 +370,15 @@ The measurements below are what this workspace observed on the hosts named; they are not a ranking, and which shape suits a given deployment is the deployment's question. +**These figures predate a correction to the probe's timing window and have not +been retaken.** The probe timed from the coordinator's clock rather than from the +producers' own, which overstated throughput, and the error grew with producer +count. The direction of the comparison survived re-measurement on the x64 host; +the absolute numbers here are optimistic and the high-producer rows most so. +Retaking them needs the two hosts named below, neither of which is the machine the +correction was measured on. See the queue-contention section of +[DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md). + **What we measured**, in ns per push, isolated regime, median of three runs. Higher producer counts oversubscribe both hosts: diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index b0fbfd7c1..aefa574ef 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -123,7 +123,10 @@ //! not is the recurrence removed outright rather than deferred. //! //! The default remains `Balanced` so that no existing caller's behaviour -//! changed when the choice was introduced. It carries the recurrence above. +//! changed when the choice was introduced. Under it, a queue driven past 2^32 +//! pushes by two or more producers can **silently lose an item** -- the defect +//! described above. `Enduring` and `Perpetual` move that point out, and `Wide` +//! removes it. //! //! **What happens.** A producer checks that there is room, is descheduled, and //! resumes after other producers have driven the position field through a @@ -287,6 +290,14 @@ //! The measurements below are what this workspace observed on the hosts named; //! they are not a ranking. //! +//! **They predate a correction to the probe's timing window and have not been +//! retaken.** The probe timed from the coordinator's clock rather than from the +//! producers' own, which overstated throughput, and the error grew with producer +//! count. The direction of the comparison survived re-measurement on the x64 +//! host; the absolute numbers here are optimistic, the high-producer rows most +//! so. Retaking them needs the two hosts named below, neither of which is the +//! machine the correction was measured on. +//! //! Measured ns per push, isolated regime, median of three. An AMD EPYC 7763 //! slice (8 cores, 16 threads) and a Snapdragon X2 Elite (12 cores, no SMT): //! diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index c3e212250..50c6df6f2 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -42,7 +42,8 @@ //! ``` //! //! The default stays `Balanced` so that introducing the choice changed no -//! existing caller's behaviour; it carries the recurrence described above. +//! existing caller's behaviour; under it the queue can silently lose an item past +//! 2^32 pushes from two or more producers. //! [`slotwise_mpsc`](crate::slotwise_mpsc) does not have this hazard under any //! layout, its positions being 64 bits on every target; [`spsc`](crate::spsc) //! never had it. The full statement is in the [crate documentation](crate). @@ -475,11 +476,16 @@ impl ClaimWord for u128 { /// The shipping division: 32 bits each. /// /// Holds 2^32 outstanding reservations and recurs after 2^32 pushes -- about -/// **37 seconds** of sustained maximum-rate pushing. This is the default -/// because it is what the shape shipped with: the reservation ceiling it buys -/// is far beyond any use this crate has seen, and it is paid for with the whole -/// of the `SH-14.1` exposure. [`Enduring`] and [`Perpetual`] trade ceiling for -/// recurrence in the other direction; [`Enduring`]'s ceiling is 65,535. +/// **37 seconds** of sustained maximum-rate pushing. Past that point, with two +/// or more producers, the queue can **silently lose an item**: that is the whole +/// of the `SH-14.1` exposure, and this layout carries it. +/// +/// It is the default because it is what the shape shipped with, not because the +/// division is a good one: the 2^32 reservation ceiling it buys is far beyond +/// any use this crate has seen, and the exposure is what pays for it. +/// [`Enduring`] and [`Perpetual`] spend that ceiling the other way -- +/// [`Enduring`] holds 65,535 outstanding reservations, [`Perpetual`] 255 -- +/// and [`Wide`] removes the recurrence rather than deferring it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Balanced; impl sealed::Sealed for Balanced {} From 364753999e382cc6d5cfb1d1b5134fcc58c42183 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 22:56:35 -0700 Subject: [PATCH 021/139] docs(platform-probes): a broken doc link, a missing capture field, and three more claims Five findings, all procedural. **A broken intra-doc link, introduced in 9c5f3e0.** `[`Wide`]` was added to `Balanced`'s rustdoc, but `Wide` is `#[cfg(feature = "dwcas")]` and `Balanced` is always built, so a default-feature rustdoc build emits `unresolved link to Wide`. Confirmed by running `cargo doc -p windows-waitable-queues --lib --no-deps` with default features, which reproduced it at the exact line, and clean afterwards. Nothing caught it: the new default-feature CI job does not run rustdoc, and the workspace docs job uses `--all-features`. Now a non-linking code reference that says why it does not link. **The report could not show it was a release build.** The capture contract requires the build profile, and the report only warned when `debug_assertions` was set -- so a captured release report carried no positive evidence of its profile, only the absence of a warning. It now prints `profile: release` (or `debug -- NOT A MEASUREMENT`) beside the sampling parameters. **The report told readers to discount its own control.** It said the two same-code rows "should agree within noise". They span 0.68-1.27x across seven runs, and D-variance-is-a-finding treats that width as an unresolved finding about the instrument -- so calling it noise normalises exactly what the line exists to expose. I cleared this in the previous round on the grounds that it described the control rather than advising a reader; that was the wrong reading. It now says the gap is dispersion, and that its width is an open question. **Two more restatements of withdrawn claims.** `D-37` still said re-apportioning the narrow word "removes the exposure for free"; the README still said "Take it when you want the recurrence gone", an imperative that survived the D-no-client-prescriptions sweep two commits ago. Also, found by reading the rendered report rather than by review: the report's own title still asked "does the array queue's tail claim contend?" -- the attribution corrected in the module doc last round and missed here. It now names push-path scaling, matching the table beneath it. Verified: default-feature and all-features rustdoc clean; fmt and clippy clean; 252 lib + 12 integration tests; release run against a binary confirmed newer than source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 36 ++++++++++++++++--- .../windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 6 ++-- .../src/reserving_mpsc.rs | 4 ++- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 1b0a435d4..953f89d45 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -34,7 +34,10 @@ fn render(out: &mut dyn std::fmt::Write) { "{}", windows_placement_probe::fingerprint::banner_line() ); - let _ = writeln!(out, "== does the array queue's tail claim contend? ==\n"); + let _ = writeln!( + out, + "== how does the array queue's push path scale with producer count? ==\n" + ); let observation = measure(); // `available_parallelism`, not the host count -- an affinity mask or job @@ -49,8 +52,20 @@ fn render(out: &mut dyn std::fmt::Write) { ), }; // The sampling parameters are capture parameters, and a figure is only - // interpretable with them -- see D-observations-not-verdicts. The dispersion - // belongs here too and is not yet carried; M4.2 covers both. + // interpretable with them -- see D-observations-not-verdicts. The build + // profile is one of them too: a captured report has to be able to show it + // was produced by a build that can measure, not merely stay silent when it + // was. The dispersion belongs here as well and is not yet carried; M4.2 + // covers it. + let _ = writeln!( + out, + "profile: {}", + if cfg!(debug_assertions) { + "debug -- NOT A MEASUREMENT, see below" + } else { + "release" + } + ); let _ = writeln!( out, "sampling: {} pushes per producer, median of {} repetitions, one untimed \ @@ -352,8 +367,21 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " configuration run twice, so they should agree within noise. They" + " configuration run twice, so the gap between them is this host's" + ); + let _ = writeln!( + out, + " same-code control: whatever it shows is dispersion, not a" + ); + let _ = writeln!( + out, + " difference between shapes. Do not read it as noise that can be" + ); + let _ = writeln!( + out, + " discounted -- its width is an open question about this" ); + let _ = writeln!(out, " instrument. They"); let _ = writeln!( out, " are no longer a control against a duplicated implementation: the" diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index f2bc35be3..bf1e3648d 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -71,7 +71,7 @@ preferred. | D-34 | **Every bounded queue surveyed is ABA-safe for one of two reasons, and this crate's `reserving_mpsc` has neither.** Either the claim counter is a whole machine word, so recurrence is unreachable -- crossbeam, concurrent-queue, thingbuf, Vyukov, SCQ's `Head`/`Tail` -- or the authorizing compare-exchange is moved onto the cell, so the decision and the write are validated together (CRQ, SCQ). Ours packs the position into a 32-bit *subfield* and authorizes with an exchange that does not cover the separately-read `head`. Nikolaev (DISC 2019, section 3) states the width assumption the field relies on and states it for **CPU-word** width, which a subfield does not satisfy; DPDK's `rte_ring` is the same protocol as ours and its published justification covers modular arithmetic only. The generalisation -- ours, unstated in any source -- is that **the atomic operation authorizing the write must cover everything the decision depended on.** Survey in [DESIGN-SESSION-2026-09-02](design-sessions/DESIGN-SESSION-2026-09-02-claim-protocol-prior-art.md); the fix is the claim-protocol replacement recorded there. | | D-35 | **Measured: the permit claim is 2.7x faster than `reserving_mpsc` at 16-32 producers, and 1.45x slower at one.** The safer claim is also the faster one everywhere contention exists, which was not the expected result -- it touches *two* shared lines where the shipping shape touches one plus a read, and [D-26](#d-26) had established that the shared line is what collapses. The mechanism is that both of its operations are unconditional read-modify-writes that never retry, where the shipping shape's compare-exchange retries once per lost race; the retries dominate long before the second line does. It is the only shape measured that gets *faster* per push as producers are added (42.8 ns at two to 19.5 at thirty-two) and the only one that stays within 1.5x of a bare contended `fetch_add`. **This decides the shape of the fix but not the fix**: the drained regime's refusal counts differ by orders of magnitude in a way this harness cannot attribute, which SH-15.5.1 exists to settle before SH-15.6 adopts anything. | | D-36 | **Superseded by [D-41](#d-41): the hazard is now a layout choice, not a defect that must ship.** The reasoning below stands as the record of why it was right to disclose rather than delay while the only known fix was the claim-protocol replacement. **0.1.0 ships SH-14.1 disclosed rather than fixed, and the disclosure is a release blocker.** Following [D-31](#d-31)'s principle -- the disclosure, not the deferral, is the decision -- because the fix is a claim-protocol replacement ([D-35](#d-35)) whose adoption is still gated on an open question, and holding the release for it would trade a *documented* hazard for an undocumented rush. **The two gaps are not equally forgiving and the text says so**: an unverified ordering is a risk of a bug, this is a known one with a computed exposure, and its failure mode is silent -- no error, panic, or counter -- so a caller can neither detect nor mitigate it. That is precisely why it may not ship in silence. Stated in the crate docs, the README, and the shape's own module docs, each leading with **"on every target, not only 32-bit ones"**, because the natural spelling "32-bit position" invites the opposite reading and SH-6.1 already had to be corrected for exactly that. The shape-selection guidance in both documents was also amended: it previously said "start with `reserving_mpsc`" with no caveat, pointing callers at the hazardous shape by default. | -| D-37 | **Partly superseded by [D-41](#d-41): the wide word ships as a *layout* behind the non-default `dwcas` feature, not as a separate `reserving_mpsc_wide` shape, and the gate is the feature rather than the target.** What stands is the reasoning below about `portable-atomic`: `default-features = false` is load-bearing, because with defaults on it silently substitutes a global lock, and D-7's burden of proof is discharged rather than waived. What does not is the shape's name and the premise that the narrow word must keep SH-14.1 -- re-apportioning the narrow word removes the exposure for free, so the wide word is no longer the only way out. **The reserving claim word ships in two widths: the narrow one on every target, the wide one only where a 128-bit exchange is genuinely lock-free.** `reserving_mpsc` keeps its packed 64-bit word, keeps SH-14.1's hazard, keeps [D-36](#d-36)'s warnings, and is **never silently swapped** for the wide shape on targets that could host one -- a contract that changes with the target is what PLATFORM INTEGRITY rule 2 forbids, and a caller who read "2^32" must get 2^32. `reserving_mpsc_wide` is the same protocol with a `u128` word split 64/64: recurrence needs 2^64 pushes, and the capacity ceiling rises to 2^62. **The gate is one line of `Cargo.toml`: `default-features = false`.** Measured, not designed -- with the default feature set `portable-atomic` compiles on i686 and silently substitutes a global lock, but with defaults off `AtomicU128` **does not exist** there (`no AtomicU128 in the root`), nor on x86_64 built without `cmpxchg16b`. It exists exactly where a native lock-free exchange is guaranteed at compile time, so the `use` statement is the gate and it fails loudly. A `cfg(target_has_atomic = "128")` would be the *wrong* gate -- it is emitted even with `cmpxchg16b` disabled -- and a `const` assertion on `is_always_lock_free()`, though genuinely const-evaluable, is redundant where the type exists and unreachable where it does not. That is the standard SH-14.2 already set when it probed i686 to confirm `AtomicU64` was lock-free before widening `slotwise_mpsc`, recording that a hidden mutex "would have made this a bad trade". [D-7](#d-7)'s burden of proof for adding a Cargo feature is **discharged, not waived**: D-7 rejected feature-gating because the only benefit was compile time, and the cost here is a third-party dependency, which dead-code elimination does not remove from `Cargo.lock` or from an auditor's review. | +| D-37 | **Partly superseded by [D-41](#d-41): the wide word ships as a *layout* behind the non-default `dwcas` feature, not as a separate `reserving_mpsc_wide` shape, and the gate is the feature rather than the target.** What stands is the reasoning below about `portable-atomic`: `default-features = false` is load-bearing, because with defaults on it silently substitutes a global lock, and D-7's burden of proof is discharged rather than waived. What does not is the shape's name and the premise that the narrow word must keep SH-14.1 -- re-apportioning the narrow word removes the exposure without a third-party dependency -- what it costs in throughput is unestablished, see [D-41](#d-41) -- so the wide word is no longer the only way out. **The reserving claim word ships in two widths: the narrow one on every target, the wide one only where a 128-bit exchange is genuinely lock-free.** `reserving_mpsc` keeps its packed 64-bit word, keeps SH-14.1's hazard, keeps [D-36](#d-36)'s warnings, and is **never silently swapped** for the wide shape on targets that could host one -- a contract that changes with the target is what PLATFORM INTEGRITY rule 2 forbids, and a caller who read "2^32" must get 2^32. `reserving_mpsc_wide` is the same protocol with a `u128` word split 64/64: recurrence needs 2^64 pushes, and the capacity ceiling rises to 2^62. **The gate is one line of `Cargo.toml`: `default-features = false`.** Measured, not designed -- with the default feature set `portable-atomic` compiles on i686 and silently substitutes a global lock, but with defaults off `AtomicU128` **does not exist** there (`no AtomicU128 in the root`), nor on x86_64 built without `cmpxchg16b`. It exists exactly where a native lock-free exchange is guaranteed at compile time, so the `use` statement is the gate and it fails loudly. A `cfg(target_has_atomic = "128")` would be the *wrong* gate -- it is emitted even with `cmpxchg16b` disabled -- and a `const` assertion on `is_always_lock_free()`, though genuinely const-evaluable, is redundant where the type exists and unreachable where it does not. That is the standard SH-14.2 already set when it probed i686 to confirm `AtomicU64` was lock-free before widening `slotwise_mpsc`, recording that a hidden mutex "would have made this a bad trade". [D-7](#d-7)'s burden of proof for adding a Cargo feature is **discharged, not waived**: D-7 rejected feature-gating because the only benefit was compile time, and the cost here is a third-party dependency, which dead-code elimination does not remove from `Cargo.lock` or from an auditor's review. | | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index b2333d29b..d3b4191b7 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -217,9 +217,9 @@ stops at 64 bits -- so the double-width compare-and-swap comes from twenty years before its claim position recurs with no dependency, though what that costs in throughput is not established, while choosing `Wide` measured slower on the whole push path as producer count rises -- near parity at one or -two, several times by thirty-two, in the isolated regime. Take -it when you want the recurrence gone as a guarantee rather than deferred by an -argument about deployment lifetimes. +two, several times by thirty-two, in the isolated regime. What `Wide` provides +that the `u64` layouts do not is the recurrence removed outright rather than +deferred. **`experimental-permit-claim`** adds `permit_mpsc`, a different claim protocol in which the decision and the operation are one atomic rather than two. It is diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 50c6df6f2..f00911112 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -485,7 +485,9 @@ impl ClaimWord for u128 { /// any use this crate has seen, and the exposure is what pays for it. /// [`Enduring`] and [`Perpetual`] spend that ceiling the other way -- /// [`Enduring`] holds 65,535 outstanding reservations, [`Perpetual`] 255 -- -/// and [`Wide`] removes the recurrence rather than deferring it. +/// and `Wide` removes the recurrence rather than deferring it. (`Wide` exists +/// only under the `dwcas` feature, so this names it without linking: an +/// intra-doc link here would not resolve in a default-feature rustdoc build.) #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Balanced; impl sealed::Sealed for Balanced {} From ea090c555d85ae070d6695c94875d3839145d333 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 23:14:31 -0700 Subject: [PATCH 022/139] fix(platform-probes): gate dwcas on the instruction, not just the architecture **The i686 fix had a hole, and the same measurement that proved i686 proves this.** Gating `dwcas` on `target_arch = "x86_64"` still enables it on an x86-64 build with `cmpxchg16b` switched off, where `portable_atomic::AtomicU128` does not exist: RUSTFLAGS="-C target-feature=-cmpxchg16b" cargo check -p windows-platform-probes --target x86_64-pc-windows-msvc error[E0433]: cannot find `AtomicU128` in `portable_atomic` `D-37` predicted exactly this and its reasoning checks out on 1.98: `rustc --print cfg --target x86_64-pc-windows-msvc -C target-feature=-cmpxchg16b` still prints `target_has_atomic="128"` while dropping `target_feature="cmpxchg16b"` -- so the feature is the discriminator and `target_has_atomic` would have been the wrong gate, as D-37 says. The manifest now carries two target sections, `cfg(all(target_arch = "x86_64", target_feature = "cmpxchg16b"))` and `cfg(target_arch = "aarch64")`, with the source `cfg`s matching. aarch64 needs no feature test -- `ldxp`/`stxp` is ARMv8-A baseline. Verified in all four configurations: the no-`cmpxchg16b` build now succeeds, i686 still succeeds, the host build still succeeds, and a release run still emits all 12 `reserving(64/64)` rows. **A methodological finding worth more than the rest of this round.** The same-code control is the `reserving_mpsc` row against the `reserving(32/32)` row, and `measure()` runs them FOUR measurements apart -- the permit shape and all three drained shapes fall between, each five repetitions of 50,000 pushes per producer. Frequency, thermal and scheduler drift across that interval lands inside the control, and inside every candidate judged against it. That is the first *specific* mechanism anyone has proposed for the 7-61% same-configuration spread; the other candidates in D-variance-is-a-finding are general. Added there, and queued as M4.4 with the same blocker M4.3 carries -- interleaving changes the measurement and obsoletes the published figures -- plus a note that M4.4 should precede M4.2's diagnosis, since an unpaired control cannot answer whether lengthening the run narrows the spread. Documentation corrections, all procedural: - **`lock cmpxchg` is x86 syntax** in documentation that covers aarch64, where the instruction is not that. The structural argument is target-neutral, so all four sites now say "the same atomic compare-exchange on the same `u64`". - **"`Reserving` exists only on `reserving_mpsc`" is false.** `spsc::Producer` implements it (`spsc.rs:1035`) and `permit_mpsc::Producer` has an inherent `reserve`. Scoped to the two MPSC shapes, with the others named. - **The `dwcas` manifest comment attributed `Wide`'s cost to `Perpetual`** -- the antecedent of "choosing it" was `Perpetual` while the figures were `Wide`'s, which recreated the withdrawn claim by misattribution. Split into two statements. - **D-26's and D-35's tables were marked for the variance floor but not for the timing correction**, though the README and rustdoc got that caveat last commit. Same class, same treatment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 30 +++++++++++++++++++ crates/windows-platform-probes/Cargo.toml | 20 +++++++++---- .../windows-platform-probes/DESIGN-NOTES.md | 10 +++++++ .../src/queue_contention.rs | 15 ++++++++-- crates/windows-waitable-queues/Cargo.toml | 16 +++++----- .../windows-waitable-queues/DESIGN-NOTES.md | 14 ++++++++- crates/windows-waitable-queues/README.md | 9 +++--- crates/windows-waitable-queues/src/lib.rs | 10 ++++--- .../src/reserving_mpsc.rs | 2 +- 9 files changed, 101 insertions(+), 25 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 22e283603..17f597e76 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -142,6 +142,36 @@ correctness in the archive. Reported by review against this branch; the comment at the slotwise twin now states what the barrier actually guarantees rather than implying the window is closed. +- [ ] **M4.4** -- Interleave each candidate with a nearby control instead of measuring the control + four runs away from it, and re-measure everything that changes. + + **Gap:** `measure()` runs, per producer count, `baseline_fetch_add`, `slotwise_mpsc`, + `reserving_mpsc`, `permit_mpsc`, then the three drained shapes, then the layout rows starting with + `reserving(32/32)`. The same-code control is the `reserving_mpsc` row against the + `reserving(32/32)` row -- **four measurements apart**, each five repetitions of 50,000 pushes per + producer. Frequency, thermal and scheduler drift across that interval is folded into the control, + and into every candidate the control is used to judge. At sixteen and thirty-two producers, where + the machine is oversubscribed and the layout differences are smallest, that is exactly where it + matters most. + + This is the first *specific* mechanism proposed for the 7-61% same-configuration spread recorded in + [DESIGN-NOTES.md](DESIGN-NOTES.md#d-variance-is-a-finding); the other candidates there are general. + Reported by review. + + **Target:** measure each candidate adjacent to a control run of the same code, or randomise and + balance the order across repetitions so drift cannot align with position in the sequence. Whichever + is chosen, the control must end up measuring the same interval the candidate did. + + **BLOCKER, same as M4.3:** interleaving changes the measurement, so every figure published in + [DESIGN-NOTES.md](DESIGN-NOTES.md) becomes a measurement of a different procedure. The item is + "change it *and* re-run the sweep *and* rewrite the sections", not a reordering. Doing it + mid-branch would invalidate figures that ten review rounds have been read against. Raised rather + than silently deferred, per the PRIME DIRECTIVE. + + **Do this before M4.2's diagnosis work if both are taken**, since a control that is not paired + cannot answer whether lengthening the run narrows the spread -- the answer would be confounded by + the same drift. + - [ ] **M2.5** -- Make the banner describe the read the body describes. Gated by M3.1 and M3.3, both landed: establishing that the middle of three discoveries agreed diff --git a/crates/windows-platform-probes/Cargo.toml b/crates/windows-platform-probes/Cargo.toml index 156a4d614..44ed1a7fe 100644 --- a/crates/windows-platform-probes/Cargo.toml +++ b/crates/windows-platform-probes/Cargo.toml @@ -190,14 +190,24 @@ serde = { version = "1.0", optional = true } # added to remove. # # `target_has_atomic = "128"` would be the WRONG condition -- `D-37` records that -# rustc emits it even with `cmpxchg16b` disabled. The architectures are named -# instead, matching the two the queue crate documents as natively lock-free: -# x86-64 via `cmpxchg16b`, aarch64 via `ldxp`/`stxp`. +# rustc emits it even with `cmpxchg16b` disabled, and that is measured here: +# `rustc --print cfg --target x86_64-pc-windows-msvc -C target-feature=-cmpxchg16b` +# still prints `target_has_atomic="128"` while dropping +# `target_feature="cmpxchg16b"`. So the feature is the discriminator, and the +# x86-64 branch tests it: naming the architecture alone would enable `dwcas` on a +# build with the instruction switched off, where `AtomicU128` does not exist and +# the queue crate fails to compile. aarch64 needs no such test -- `ldxp`/`stxp` is +# ARMv8-A baseline and requires no target feature. # -# `src/queue_contention.rs` gates the `Wide` rows on the SAME condition. The two +# `src/queue_contention.rs` gates the `Wide` rows on the SAME conditions. The two # must agree; the source side names this comment so a reader changing one finds # the other. -[target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies] +[target.'cfg(all(target_arch = "x86_64", target_feature = "cmpxchg16b"))'.dependencies] +windows-waitable-queues = { path = "../windows-waitable-queues", features = [ + "dwcas", +] } + +[target.'cfg(target_arch = "aarch64")'.dependencies] windows-waitable-queues = { path = "../windows-waitable-queues", features = [ "dwcas", ] } diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 6832e7996..d9c5df452 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -134,6 +134,16 @@ of them are live here: - **The wrong instrument for the variable.** A probe that moves several things at once cannot attribute a difference to the one under test. +- **A control that is not a paired control.** This probe's same-code control is + the `reserving_mpsc` row against the `reserving(32/32)` row, and `measure()` + runs them **four measurements apart** -- the permit shape and all three drained + shapes fall between them, each five repetitions of 50,000 pushes per producer. + Any frequency, thermal or scheduler drift across that interval lands inside the + control, and by the same token inside every candidate row it is compared + against. This is a specific, mechanical candidate for the spread above rather + than a general worry, and it was proposed by review rather than found here. + Queued as M4.4 in [CHECKLIST.md](CHECKLIST.md), because interleaving changes the + measurement and so obsoletes the published figures. - **A defect in the probe itself.** This crate has already shipped one -- the timing window that read the coordinator's clock rather than the producers'. That defect was invisible in the numbers until it was found by reading, and it diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 3dd9539ff..5b7f3f2f7 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -94,7 +94,10 @@ use windows_waitable_queues::reserving_mpsc::{Balanced, ClaimLayout, Enduring, P /// why enabling the feature unconditionally breaks the workspace's deliberately /// supported `i686-pc-windows-msvc` build. Changing one without the other yields /// either a missing type or an unused feature. -#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] +#[cfg(any( + all(target_arch = "x86_64", target_feature = "cmpxchg16b"), + target_arch = "aarch64" +))] use windows_waitable_queues::reserving_mpsc::Wide; #[cfg(test)] @@ -260,7 +263,10 @@ pub fn measure() -> Observation { // Gated on the architectures where a 128-bit exchange is native; see the // `Wide` import above. `#[cfg]` governs only the statement that follows // it, so each of the two pushes carries its own. - #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] + #[cfg(any( + all(target_arch = "x86_64", target_feature = "cmpxchg16b"), + target_arch = "aarch64" + ))] isolated.push(median_run(shapes::CLAIM_WIDE, producers, |count| { time_isolated_layout::(count) })); @@ -274,7 +280,10 @@ pub fn measure() -> Observation { drained.push(median_run(shapes::CLAIM_PERPETUAL, producers, |count| { time_drained_layout::(count) })); - #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] + #[cfg(any( + all(target_arch = "x86_64", target_feature = "cmpxchg16b"), + target_arch = "aarch64" + ))] drained.push(median_run(shapes::CLAIM_WIDE, producers, |count| { time_drained_layout::(count) })); diff --git a/crates/windows-waitable-queues/Cargo.toml b/crates/windows-waitable-queues/Cargo.toml index ad13ceafd..da48a6684 100644 --- a/crates/windows-waitable-queues/Cargo.toml +++ b/crates/windows-waitable-queues/Cargo.toml @@ -57,13 +57,15 @@ experimental-permit-claim = [] # while still compiling. With them off, `AtomicU128` does not exist on such a # target and the build fails naming it. # -# `Perpetual` reaches roughly twenty years -# before its claim position recurs, on a plain `AtomicU64`; what that costs in -# throughput is not established, and choosing it measured slower on the -# whole push path as producer count rises -- near parity at one or two, several -# times by thirty-two, in the isolated regime. The probe times the complete push, -# so that is the layout's effect on that path, not the exchange in isolation. See -# `ClaimLayout` for the comparison. +# This feature adds only the `Wide` layout. `Wide` measured slower on the whole +# push path as producer count rises -- near parity at one or two, several times by +# thirty-two, in the isolated regime. The probe times the complete push, so that +# is the layout's effect on that path, not the exchange in isolation. +# +# `Perpetual` needs no feature: it reaches roughly twenty years before its claim +# position recurs on a plain `AtomicU64`, and what THAT costs in throughput is not +# established -- a separate question from the figures above, which are `Wide`'s. +# See `ClaimLayout` for both. dwcas = ["dep:portable-atomic"] [lib] diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index bf1e3648d..28d7f7570 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** `lock cmpxchg` on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and choosing it measured slower on the whole push path as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and choosing it measured slower on the whole push path as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered @@ -856,6 +856,12 @@ and any difference here smaller than that control should not be treated as established. See [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). +**These figures also predate the correction to the probe's timing window and have +not been retaken.** The probe timed from the coordinator's clock rather than from +the producers' own, which overstated throughput by a margin that grew with +producer count. Read the absolute values as optimistic, the high-producer rows +most so. + **Note the architecture**: every previous measurement in this workspace was taken on the ARM64 development machine, so these numbers fill the x64 gap rather than extending the ARM64 record, and the two are not interchangeable. @@ -1269,6 +1275,12 @@ control at 0.68-1.27x, so three agreeing runs establish less than the phrase suggests. See [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). +**These figures also predate the correction to the probe's timing window and have +not been retaken.** The probe timed from the coordinator's clock rather than from +the producers' own, which overstated throughput by a margin that grew with +producer count. Read the absolute values as optimistic, the high-producer rows +most so. + ### Isolated regime -- producers only, nothing ever refused The cleanest measurement of the claim, because nothing else touches the queue. Nanoseconds per push: diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index d3b4191b7..1e3bf1524 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -186,7 +186,7 @@ proportionally longer to reach its wrap. - **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty years out. **What it costs in throughput is not established** -- it issues the - same `lock cmpxchg` on the same `u64` as the + same atomic compare-exchange on the same `u64` as the default, and measured indistinguishable from it at low producer counts; at high counts the difference did not clearly exceed the run-to-run variation of the same code measured twice. @@ -360,9 +360,10 @@ both rather than picking one for you. The mechanism is in [the section on recurrence](#how-long-reserving_mpsc-runs-before-its-claim-position-recurs) above. -- **`reserve` exists only on `reserving_mpsc`**; `slotwise_mpsc` structurally - cannot offer it. That no longer forces a trade against the recurrence, since - naming a layout addresses it. +- **Of the two MPSC shapes, only `reserving_mpsc` offers `reserve`**; + `slotwise_mpsc` structurally cannot. (`spsc` has it too, and the experimental + `permit_mpsc` exposes its own.) That no longer forces a trade against the + recurrence, since naming a layout addresses it. - **`spsc` requires exactly one producer and one consumer**, and does less work than either MPSC shape because of it. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index aefa574ef..6fd7bff4f 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -158,7 +158,7 @@ //! //! - **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty //! years out. **What it costs in throughput is not -//! established** -- it issues the same `lock cmpxchg` on the same `u64` as +//! established** -- it issues the same atomic compare-exchange on the same //! the default, and measured indistinguishable from it at low producer //! counts; at high counts the difference did not clearly exceed the //! run-to-run variation of the same code measured twice. @@ -281,9 +281,11 @@ //! positions are 64 bits under every configuration, and naming a deeper layout //! on [`reserving_mpsc`] moves the recurrence out. The mechanism is in the //! section above. -//! - **[`Reserving`] exists only on [`reserving_mpsc`]**; [`slotwise_mpsc`] -//! structurally cannot offer it. Naming a layout addresses the recurrence, so -//! that no longer trades against this capability. +//! - **Of the two MPSC shapes, only [`reserving_mpsc`] implements +//! [`Reserving`]**; [`slotwise_mpsc`] structurally cannot. ([`spsc`] +//! implements it too, and the experimental `permit_mpsc` exposes its own +//! `reserve`.) Naming a layout addresses the recurrence, so that no longer +//! trades against this capability. //! - **[`spsc`] requires exactly one producer and one consumer**, and does less //! work than either MPSC shape because of it. //! diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index f00911112..345f555eb 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -186,7 +186,7 @@ use crate::options::Options; /// measured, so treat these as a floor on time rather than a forecast. /// /// **Choosing a deeper position is the same instruction on the same word.** All -/// three issue the same `lock cmpxchg` on the same `u64` and differ only in +/// three issue the same atomic compare-exchange on the same `u64` and differ /// shift and mask constants, so there is no structural reason for one to be /// slower. **What that costs in throughput is not established**: a probe /// comparing them found them indistinguishable at low producer counts, and at From d9739242822abdc2b4d02678dad14e63a2bf2be3 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 23:29:11 -0700 Subject: [PATCH 023/139] docs(waitable-queues): fix a broken sentence, a too-strong claim, and a construction Mostly grammar, with one substantive correction and one sentence I broke. **A sentence I broke in ea090c5.** Making the instruction wording target-neutral dropped two words from the crate rustdoc, leaving "the same atomic compare-exchange on the same the default" -- the `u64` and the "as" went with the replaced text. The README's copy survived intact, which is why it read correctly and this one did not. **A claim that was too strong.** Both selection blurbs said that naming a layout means wanting `reserve` "no longer forces a trade against the recurrence". The trade is not gone, it changes axis: a deeper position is paid for with a lower ceiling on outstanding reservations -- 65,535 under `Enduring` and 255 under `Perpetual`, against 2^32 under the default, as `reserving_mpsc.rs:478-517` states. A caller who needs more outstanding reservations than that still chooses between the ceiling and the shorter recurrence. Both sites now say so. **Six instances of one construction.** "choosing it measured slower", "`Wide` measured slower", "the 128-bit exchange measured slower" all make the subject the thing performing the measurement, and the last also names the exchange as the measured subject when the probe timed the whole push path. All now passive: the push path *was measured as* slower under the layout. Two related omissions fixed in the same pass -- "and measured indistinguishable" now reads "was measured as indistinguishable". The instance of "and measured indistinguishable outside noise" inside D-41 is left alone: it is a quotation of the withdrawn clause, and altering it would misrepresent what was withdrawn. Verified: fmt and clippy clean; 13 doctests including the compiled README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/Cargo.toml | 4 ++-- .../windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 17 +++++++++------- crates/windows-waitable-queues/src/lib.rs | 20 ++++++++++--------- .../src/reserving_mpsc.rs | 4 ++-- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/crates/windows-waitable-queues/Cargo.toml b/crates/windows-waitable-queues/Cargo.toml index da48a6684..903514f43 100644 --- a/crates/windows-waitable-queues/Cargo.toml +++ b/crates/windows-waitable-queues/Cargo.toml @@ -57,8 +57,8 @@ experimental-permit-claim = [] # while still compiling. With them off, `AtomicU128` does not exist on such a # target and the build fails naming it. # -# This feature adds only the `Wide` layout. `Wide` measured slower on the whole -# push path as producer count rises -- near parity at one or two, several times by +# This feature adds only the `Wide` layout. The whole push path was measured as +# slower under it as producer count rises -- near parity at one or two, several times by # thirty-two, in the isolated regime. The probe times the complete push, so that # is the layout's effect on that path, not the exchange in isolation. # diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 28d7f7570..6401c07dd 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and choosing it measured slower on the whole push path as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 1e3bf1524..9eefff7fd 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -146,7 +146,7 @@ one to be slower -- but **what that costs in throughput is not established**: a probe comparing them found them indistinguishable at low producer counts, and at high counts a difference that did not clearly exceed the run-to-run variation of the same code measured twice. `Wide` is a separate matter: it needs a 128-bit exchange, -and choosing it measured slower on the whole push path as producer count rises +and the whole push path was measured as slower under it as producer count rises -- near parity at one or two, several times by thirty-two, in the isolated regime -- and it is the only thing in this crate that costs a third-party dependency. @@ -186,8 +186,8 @@ proportionally longer to reach its wrap. - **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty years out. **What it costs in throughput is not established** -- it issues the - same atomic compare-exchange on the same `u64` as the - default, and measured indistinguishable from it at low producer counts; at + same atomic compare-exchange on the same `u64` as the default, and was measured + as indistinguishable from it at low producer counts; at high counts the difference did not clearly exceed the run-to-run variation of the same code measured twice. - **`slotwise_mpsc` does not have this hazard** under any layout. Its positions @@ -215,8 +215,8 @@ dependency: Rust's standard library has no 128-bit atomic -- `core::sync::atomic stops at 64 bits -- so the double-width compare-and-swap comes from `portable-atomic`. `Perpetual` reaches roughly twenty years before its claim position recurs with no dependency, though what -that costs in throughput is not established, while choosing `Wide` measured -slower on the whole push path as producer count rises -- near parity at one or +that costs in throughput is not established, while under `Wide` the whole push +path was measured as slower as producer count rises -- near parity at one or two, several times by thirty-two, in the isolated regime. What `Wide` provides that the `u64` layouts do not is the recurrence removed outright rather than deferred. @@ -362,8 +362,11 @@ both rather than picking one for you. above. - **Of the two MPSC shapes, only `reserving_mpsc` offers `reserve`**; `slotwise_mpsc` structurally cannot. (`spsc` has it too, and the experimental - `permit_mpsc` exposes its own.) That no longer forces a trade against the - recurrence, since naming a layout addresses it. + `permit_mpsc` exposes its own.) Wanting `reserve` no longer means accepting the + default layout's recurrence, but the trade is not gone -- it changes axis: a + deeper position is paid for with a lower ceiling on outstanding reservations, + 65,535 under `Enduring` and 255 under `Perpetual` against 2^32 under the + default. - **`spsc` requires exactly one producer and one consumer**, and does less work than either MPSC shape because of it. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 6fd7bff4f..a1caf449e 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -114,8 +114,8 @@ //! established**: a probe comparing them found them indistinguishable at low //! producer counts, and at high counts a difference that did not clearly exceed //! the run-to-run variation of the same code measured twice. `Wide` is a separate -//! matter: it needs a 128-bit exchange, and choosing it measured slower on the -//! whole push path as producer count rises -- near parity at one or two, +//! matter: it needs a 128-bit exchange, and the whole push path was measured as +//! slower under it as producer count rises -- near parity at one or two, //! several times by thirty-two, in the isolated regime -- and it is the only //! thing in //! this crate @@ -157,11 +157,11 @@ //! **What bears on it.** //! //! - **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty -//! years out. **What it costs in throughput is not -//! established** -- it issues the same atomic compare-exchange on the same -//! the default, and measured indistinguishable from it at low producer -//! counts; at high counts the difference did not clearly exceed the -//! run-to-run variation of the same code measured twice. +//! years out. **What it costs in throughput is not established** -- it issues +//! the same atomic compare-exchange on the same `u64` as the default, and was +//! measured as indistinguishable from it at low producer counts; at high +//! counts the difference did not clearly exceed the run-to-run variation of +//! the same code measured twice. //! - **[`slotwise_mpsc`] does not have this hazard** under any layout. Its //! positions are 64 bits on every target, so the equivalent wrap needs 2^64 //! claims. It does not offer [`Reserving`]. @@ -284,8 +284,10 @@ //! - **Of the two MPSC shapes, only [`reserving_mpsc`] implements //! [`Reserving`]**; [`slotwise_mpsc`] structurally cannot. ([`spsc`] //! implements it too, and the experimental `permit_mpsc` exposes its own -//! `reserve`.) Naming a layout addresses the recurrence, so that no longer -//! trades against this capability. +//! `reserve`.) Wanting it no longer means accepting the default layout's +//! recurrence, but the trade is not gone -- it changes axis: a deeper position +//! is paid for with a lower ceiling on outstanding reservations, 65,535 under +//! `Enduring` and 255 under `Perpetual` against 2^32 under the default. //! - **[`spsc`] requires exactly one producer and one consumer**, and does less //! work than either MPSC shape because of it. //! diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 345f555eb..786d0c3cf 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -535,8 +535,8 @@ impl ClaimLayout for Perpetual { /// pushes to recur, which no deployment reaches -- not "not for twenty years", /// but not at all. /// -/// The 128-bit exchange measured slower on the whole push path than a `u64` -/// layout does, and the difference **grows with producer count** -- near parity +/// The whole push path was measured as slower under this layout than under a +/// `u64` one, and the difference **grows with producer count** -- near parity /// at one or two, several times by thirty-two, in the isolated regime on one /// x86-64 host; against a draining consumer the difference fell inside that /// host's same-code control and could not be called at all. The probe times the From bba97ba9c9229cd6ce7f9568b6647b1277ff499d Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 23:43:51 -0700 Subject: [PATCH 024/139] docs: derive the layout count, correct the ceiling, and order M4.4 before M4.2 Seven findings, each a statement that stopped being true when something else changed. **The report hardcoded "Four apportionments".** On a target without a native 128-bit exchange the `Wide` rows are cfg-elided, so the count is three and the sentence was false. Replaced with a count derived from what was actually measured, which cannot drift. Verified in both directions by building and running each configuration: the host build reports 4 with 12 `reserving(64/64)` rows, and a `-C target-feature=-cmpxchg16b` build reports 3 with none. The `64/64` column header is left in place on that build -- its cells render `--`, the documented marker for a measurement not taken, which discloses that the layout exists rather than hiding it. **The reservation ceiling was rounded, and inconsistently.** `MAX_RESERVED` caps at `u32::MAX`, so `Balanced` holds 2^32 - 1 outstanding reservations, not 2^32 -- while the two figures written beside it, 65,535 and 255, are exact. Corrected in the two selection blurbs I added last commit and in `Balanced`'s own rustdoc, where the same rounding was pre-existing. **Two statements that my own gate tightening invalidated.** The design note still said the wide rows are measured "where a 128-bit exchange is native -- x86-64 and aarch64", but ea090c5 narrowed the gate to the target FEATURE: an x86-64 build with `cmpxchg16b` disabled omits them deliberately. And the stand-in section gave the same-code control as 0.69-1.27x, mixing the isolated minimum with the drained maximum; the combined range is 0.68-1.27x, which is what every other site says. **M4.4 said it must precede M4.2 and was filed after it.** Moved ahead, with the reason restated from its new position and a reciprocal note on M4.2 explaining why it appears second despite the lower number. IDs are unchanged -- this milestone already documents why they stay stable. Verified: fmt and clippy clean; 13 doctests including the compiled README; probe run in both target-feature configurations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 65 ++++++++++--------- .../windows-platform-probes/DESIGN-NOTES.md | 8 ++- .../src/bin/queue_contention.rs | 22 ++++++- crates/windows-waitable-queues/README.md | 2 +- crates/windows-waitable-queues/src/lib.rs | 2 +- .../src/reserving_mpsc.rs | 7 +- 6 files changed, 65 insertions(+), 41 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 17f597e76..c6bd40cc9 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -67,9 +67,44 @@ correctness in the archive. own arithmetic does NOT belong, and the honest outcome for such a one is a line in the module header saying so by name rather than a silent absence. +- [ ] **M4.4** -- Interleave each candidate with a nearby control instead of measuring the control + four runs away from it, and re-measure everything that changes. + + **Gap:** `measure()` runs, per producer count, `baseline_fetch_add`, `slotwise_mpsc`, + `reserving_mpsc`, `permit_mpsc`, then the three drained shapes, then the layout rows starting with + `reserving(32/32)`. The same-code control is the `reserving_mpsc` row against the + `reserving(32/32)` row -- **four measurements apart**, each five repetitions of 50,000 pushes per + producer. Frequency, thermal and scheduler drift across that interval is folded into the control, + and into every candidate the control is used to judge. At sixteen and thirty-two producers, where + the machine is oversubscribed and the layout differences are smallest, that is exactly where it + matters most. + + This is the first *specific* mechanism proposed for the 7-61% same-configuration spread recorded in + [DESIGN-NOTES.md](DESIGN-NOTES.md#d-variance-is-a-finding); the other candidates there are general. + Reported by review. + + **Target:** measure each candidate adjacent to a control run of the same code, or randomise and + balance the order across repetitions so drift cannot align with position in the sequence. Whichever + is chosen, the control must end up measuring the same interval the candidate did. + + **BLOCKER, same as M4.3:** interleaving changes the measurement, so every figure published in + [DESIGN-NOTES.md](DESIGN-NOTES.md) becomes a measurement of a different procedure. The item is + "change it *and* re-run the sweep *and* rewrite the sections", not a reordering. Doing it + mid-branch would invalidate figures that ten review rounds have been read against. Raised rather + than silently deferred, per the PRIME DIRECTIVE. + + **It is placed ahead of M4.2 deliberately**, since a control that is not paired cannot answer + whether lengthening the run narrows the spread -- that answer would be confounded by the same + drift this item removes. Taking M4.2 first would produce a diagnosis nobody could trust. + - [ ] **M4.2** -- Give the measurement probes the controls needed to act on a dispersion finding, so "gather more data along this axis" does not require editing a `const` and rebuilding. + **Ordered after M4.4, which is why it appears second despite the lower number.** The first + diagnostic step this item unblocks is "lengthen the run and see whether the control narrows", and + that cannot be read while the control is measured four runs away from its candidate -- drift would + confound it either way. + **This item is deliberately small in software and large in guidance.** The diagnostic method belongs in [DESIGN-NOTES.md](DESIGN-NOTES.md) -- see [What to try first, and how to tell when you have reached the @@ -142,36 +177,6 @@ correctness in the archive. Reported by review against this branch; the comment at the slotwise twin now states what the barrier actually guarantees rather than implying the window is closed. -- [ ] **M4.4** -- Interleave each candidate with a nearby control instead of measuring the control - four runs away from it, and re-measure everything that changes. - - **Gap:** `measure()` runs, per producer count, `baseline_fetch_add`, `slotwise_mpsc`, - `reserving_mpsc`, `permit_mpsc`, then the three drained shapes, then the layout rows starting with - `reserving(32/32)`. The same-code control is the `reserving_mpsc` row against the - `reserving(32/32)` row -- **four measurements apart**, each five repetitions of 50,000 pushes per - producer. Frequency, thermal and scheduler drift across that interval is folded into the control, - and into every candidate the control is used to judge. At sixteen and thirty-two producers, where - the machine is oversubscribed and the layout differences are smallest, that is exactly where it - matters most. - - This is the first *specific* mechanism proposed for the 7-61% same-configuration spread recorded in - [DESIGN-NOTES.md](DESIGN-NOTES.md#d-variance-is-a-finding); the other candidates there are general. - Reported by review. - - **Target:** measure each candidate adjacent to a control run of the same code, or randomise and - balance the order across repetitions so drift cannot align with position in the sequence. Whichever - is chosen, the control must end up measuring the same interval the candidate did. - - **BLOCKER, same as M4.3:** interleaving changes the measurement, so every figure published in - [DESIGN-NOTES.md](DESIGN-NOTES.md) becomes a measurement of a different procedure. The item is - "change it *and* re-run the sweep *and* rewrite the sections", not a reordering. Doing it - mid-branch would invalidate figures that ten review rounds have been read against. Raised rather - than silently deferred, per the PRIME DIRECTIVE. - - **Do this before M4.2's diagnosis work if both are taken**, since a control that is not paired - cannot answer whether lengthening the run narrows the spread -- the answer would be confounded by - the same drift. - - [ ] **M2.5** -- Make the banner describe the read the body describes. Gated by M3.1 and M3.3, both landed: establishing that the middle of three discoveries agreed diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index d9c5df452..10c4a5ed6 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -761,8 +761,10 @@ should be read as measurements of the consumer. Measured by `probe-queue-contention` on one host, `x86_64-pc-windows-msvc`. Four apportionments of `reserving_mpsc`'s claim word: 32/32, 16/48 and 8/56 over `AtomicU64`, and 64/64 over `AtomicU128`. The last is measured only where a -128-bit exchange is native -- x86-64 and aarch64 -- so on a target without one -the report carries the other three and leaves its column empty. +128-bit exchange is native: aarch64, and x86-64 **built with `cmpxchg16b`** -- +the gate is the target feature rather than the architecture, because an x86-64 +build with the instruction switched off has no `AtomicU128` either. On a target +without one the report carries the other three and leaves its column empty. **These were duplicated scaffolding when the measurement was taken, and they ship now.** The layouts were built as copies so the shipping crate was not @@ -791,7 +793,7 @@ branch was measured as though it were the algorithm. The reasoning was that both layouts issue the same `lock cmpxchg` on the same `u64`, so only the shift and mask constants differ, and the table above was read as confirming it. The table cannot carry that weight: these are single-run -figures, and the same-code control measured later ranges 0.69-1.27x, which is +figures, and the same-code control measured later ranges 0.68-1.27x, which is wider than most of the differences being called "noise" -- note that this very table has 16/48 at 1.14x and 1.21x while the prose beneath it says "within noise". See diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 953f89d45..c83c1cd29 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -260,9 +260,25 @@ fn render(out: &mut dyn std::fmt::Write) { // Question 3: what does the claim word's apportionment and width cost? let _ = writeln!(out, "\n 3. claim-word layout\n"); - let _ = writeln!( - out, - " Four apportionments of reserving_mpsc's claim word, measured on" + // Counted from what was actually measured rather than written as a literal: + // the 64/64 rows are cfg-elided on a target with no native 128-bit exchange, + // and a hardcoded "four" would be false there. + let layouts_measured = [ + shapes::CLAIM_NARROW, + shapes::CLAIM_DEEP, + shapes::CLAIM_PERPETUAL, + shapes::CLAIM_WIDE, + ] + .iter() + .filter(|shape| { + observation + .find(&observation.isolated, shape, PRODUCER_COUNTS[0]) + .is_some() + }) + .count(); + let _ = writeln!( + out, + " {layouts_measured} apportionments of reserving_mpsc's claim word, measured on" ); let _ = writeln!( out, diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 9eefff7fd..05499c174 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -365,7 +365,7 @@ both rather than picking one for you. `permit_mpsc` exposes its own.) Wanting `reserve` no longer means accepting the default layout's recurrence, but the trade is not gone -- it changes axis: a deeper position is paid for with a lower ceiling on outstanding reservations, - 65,535 under `Enduring` and 255 under `Perpetual` against 2^32 under the + 65,535 under `Enduring` and 255 under `Perpetual` against `u32::MAX` under the default. - **`spsc` requires exactly one producer and one consumer**, and does less work than either MPSC shape because of it. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index a1caf449e..aeccba235 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -287,7 +287,7 @@ //! `reserve`.) Wanting it no longer means accepting the default layout's //! recurrence, but the trade is not gone -- it changes axis: a deeper position //! is paid for with a lower ceiling on outstanding reservations, 65,535 under -//! `Enduring` and 255 under `Perpetual` against 2^32 under the default. +//! `Enduring` and 255 under `Perpetual` against `u32::MAX` under the default. //! - **[`spsc`] requires exactly one producer and one consumer**, and does less //! work than either MPSC shape because of it. //! diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 786d0c3cf..b483f508d 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -475,14 +475,15 @@ impl ClaimWord for u128 { /// The shipping division: 32 bits each. /// -/// Holds 2^32 outstanding reservations and recurs after 2^32 pushes -- about +/// Holds [`u32::MAX`] outstanding reservations and recurs after 2^32 pushes -- +/// about /// **37 seconds** of sustained maximum-rate pushing. Past that point, with two /// or more producers, the queue can **silently lose an item**: that is the whole /// of the `SH-14.1` exposure, and this layout carries it. /// /// It is the default because it is what the shape shipped with, not because the -/// division is a good one: the 2^32 reservation ceiling it buys is far beyond -/// any use this crate has seen, and the exposure is what pays for it. +/// division is a good one: the [`u32::MAX`] reservation ceiling it buys is far +/// beyond any use this crate has seen, and the exposure is what pays for it. /// [`Enduring`] and [`Perpetual`] spend that ceiling the other way -- /// [`Enduring`] holds 65,535 outstanding reservations, [`Perpetual`] 255 -- /// and `Wide` removes the recurrence rather than deferring it. (`Wide` exists From a99108f8e9dc3f1c7091e68c0ffa08a21b6dc9eb Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 23:55:37 -0700 Subject: [PATCH 025/139] docs: Wide moves the recurrence to 2^64, it does not remove it Six findings, two of them factual errors about code. **`Wide` was described as removing the recurrence outright, in five places.** Its `POSITION_BITS` is 64, so the position recurs after 2^64 pushes -- which the layout tables have always said, and which the prose then contradicted. That is a stronger guarantee than the implementation provides, and it was published in the README, the crate rustdoc, `ClaimLayout`'s docs, the `reserving_mpsc` module header, and the probe's own report. All now say the recurrence moves to 2^64, which no deployment reaches, rather than that it is eliminated. Four of those were mine, written while removing client prescriptions. The fifth was pre-existing and is the likely source: `Wide`'s own rustdoc gave the bound correctly and then glossed it as "not 'not for twenty years', but not at all", which is where the "outright" reading came from. **A factual error about the queue's implementation.** The comment explaining why this probe measures both shapes with default metrics said high-water tracking "adds a load of the consumer's position". It does not: `reserving_mpsc::publish` loads `head` **unconditionally**, because the slot write needs that acquire edge whether or not anything is measured -- the comment at that load says exactly this, and the code confirms it (the load precedes the `tracks_high_water()` branch and its result is reused by it). What the switch adds is the depth arithmetic and the metric update. The handicap is real and the correction stands; it is smaller than claimed and in a different place. **The reservation ceilings were inconsistent between prose and tables.** Last commit corrected the prose to `u32::MAX` but left three layout tables saying 2^32, so callers had two ceilings for the same layout -- and the tables were internally inconsistent too, giving exact counts for `Enduring` (65,535) and `Perpetual` (255) beside rounded ones for `Balanced` and `Wide`. All four entries are now the exact 4,294,967,295. Verified: fmt and clippy clean; 13 doctests including the compiled README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 2 +- .../src/queue_contention.rs | 17 ++++++++++++----- crates/windows-waitable-queues/README.md | 9 +++++---- crates/windows-waitable-queues/src/lib.rs | 7 ++++--- .../src/reserving_mpsc.rs | 19 ++++++++++--------- 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index c83c1cd29..91a9776e5 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -334,7 +334,7 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " push path -- what removing the recurrence outright costs, against" + " push path -- what moving the recurrence to 2^64 costs, against" ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 5b7f3f2f7..1d996ee84 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -620,11 +620,18 @@ fn time_drained_reserving(producers: usize) -> Repetition { // **Defaults on both sides, and that is a correction.** This row previously // enabled high-water tracking here and nowhere else, to "also price the // switch M31.4 made opt-in". But the number it feeds is presented as the - // cost of *reservation*, and tracking adds an unrelated operation to this - // shape's push path alone -- a load of the consumer's position, which is - // exactly the shared line the other shape's push is built to avoid - // touching. The ratio therefore measured reservation plus a handicap, with - // no way for a reader to separate them. + // cost of *reservation*, and tracking adds work to this shape's push path + // alone, so the ratio measured reservation plus a handicap with no way for a + // reader to separate them. + // + // **What the handicap actually is, corrected:** an earlier version of this + // comment said tracking adds a load of the consumer's position. It does not. + // `reserving_mpsc::publish` loads `head` **unconditionally** -- the slot + // write needs that acquire edge whether or not anything is measured, as the + // comment at that load says in as many words. What the switch adds is the + // depth arithmetic and the metric update on the far side of a branch that is + // taken either way. Smaller than claimed, and still not part of what this row + // is presented as measuring. // // Nothing consumes the high-water figure here either, so the tracking was // paying a cost to produce a number nobody read. Pricing that switch is a diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 05499c174..f3734b873 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -125,10 +125,10 @@ positions: | Layout | Outstanding reservations | Pushes to recurrence | At sustained maximum rate | |---|---|---|---| -| `Balanced` (default) | 2^32 | 2^32 | about 37 seconds | +| `Balanced` (default) | 4,294,967,295 | 2^32 | about 37 seconds | | `Enduring` | 65,535 | 2^48 | about 28 days | | `Perpetual` | 255 | 2^56 | about 20 years | -| `Wide` (needs `dwcas`) | 2^32 | 2^64 | unreachable | +| `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | ```rust use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; @@ -218,8 +218,9 @@ twenty years before its claim position recurs with no dependency, though what that costs in throughput is not established, while under `Wide` the whole push path was measured as slower as producer count rises -- near parity at one or two, several times by thirty-two, in the isolated regime. What `Wide` provides -that the `u64` layouts do not is the recurrence removed outright rather than -deferred. +that the `u64` layouts do not is a 64-bit position: the recurrence moves to +2^64 pushes, which no deployment reaches, rather than to a horizon measured in +years. **`experimental-permit-claim`** adds `permit_mpsc`, a different claim protocol in which the decision and the operation are one atomic rather than two. It is diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index aeccba235..13fdadbf6 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -93,10 +93,10 @@ //! //! | Layout | Outstanding reservations | Pushes to recurrence | At sustained maximum rate | //! |---|---|---|---| -//! | `Balanced` (default) | 2^32 | 2^32 | about 37 seconds | +//! | `Balanced` (default) | 4,294,967,295 | 2^32 | about 37 seconds | //! | `Enduring` | 65,535 | 2^48 | about 28 days | //! | `Perpetual` | 255 | 2^56 | about 20 years | -//! | `Wide` (needs `dwcas`) | 2^32 | 2^64 | unreachable | +//! | `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | //! //! ``` //! use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; @@ -120,7 +120,8 @@ //! thing in //! this crate //! that costs a third-party dependency. What it provides that `Perpetual` does -//! not is the recurrence removed outright rather than deferred. +//! not is a 64-bit position: the recurrence moves to 2^64 pushes, which no +//! deployment reaches, rather than to a horizon measured in years. //! //! The default remains `Balanced` so that no existing caller's behaviour //! changed when the choice was introduced. Under it, a queue driven past 2^32 diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index b483f508d..f3dd9e656 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -30,8 +30,8 @@ //! ceiling of 255 -- it is the same exchange on //! the same word, differing only in shift constants, though what that costs in //! throughput is not established (see [`ClaimLayout`]). [`Enduring`] sits between -//! them, and the `dwcas` feature adds a 128-bit word that removes the -//! recurrence outright. +//! them, and the `dwcas` feature adds a 128-bit word whose 64-bit position moves +//! the recurrence to 2^64 pushes, which no deployment reaches. //! //! ``` //! use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; @@ -175,7 +175,7 @@ use crate::options::Options; /// /// | Layout | reserved / position | Outstanding reservations | Pushes to recurrence | /// |---|---|---|---| -/// | [`Balanced`] | 32 / 32 | 2^32 | 2^32 | +/// | [`Balanced`] | 32 / 32 | 4,294,967,295 | 2^32 | /// | [`Enduring`] | 16 / 48 | 65,535 | 2^48 | /// | [`Perpetual`] | 8 / 56 | 255 | 2^56 | /// @@ -486,7 +486,7 @@ impl ClaimWord for u128 { /// beyond any use this crate has seen, and the exposure is what pays for it. /// [`Enduring`] and [`Perpetual`] spend that ceiling the other way -- /// [`Enduring`] holds 65,535 outstanding reservations, [`Perpetual`] 255 -- -/// and `Wide` removes the recurrence rather than deferring it. (`Wide` exists +/// and `Wide` moves it to 2^64 pushes rather than to a horizon in years. (`Wide` exists /// only under the `dwcas` feature, so this names it without linking: an /// intra-doc link here would not resolve in a default-feature rustdoc build.) #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -532,9 +532,9 @@ impl ClaimLayout for Perpetual { /// A 128-bit claim word: 64 bits of position, and the count in the other half. /// /// Requires the `dwcas` feature, which is what brings in the `portable-atomic` -/// dependency this crate otherwise does not have. The position needs 2^64 -/// pushes to recur, which no deployment reaches -- not "not for twenty years", -/// but not at all. +/// dependency this crate otherwise does not have. The position is 64 bits, so it +/// recurs after 2^64 pushes -- a bound that exists but that no deployment +/// reaches, rather than the twenty years [`Perpetual`] buys. /// /// The whole push path was measured as slower under this layout than under a /// `u64` one, and the difference **grows with producer count** -- near parity @@ -549,8 +549,9 @@ impl ClaimLayout for Perpetual { /// /// [`Perpetual`] reaches about twenty years on a plain `AtomicU64`, and what /// that costs in throughput is not established -- see [`ClaimLayout`]. What this -/// layout provides that the others do not is the recurrence removed outright -/// rather than deferred. +/// layout provides that the others do not is a 64-bit position: the recurrence +/// moves to 2^64 pushes, which no deployment reaches, rather than to a horizon +/// measured in years. /// /// The reservation ceiling is [`u32::MAX`] rather than the 64 bits the field /// could hold, because the count is reported to callers as a `u32`. From b196db62eca9f5873d7927412752e892e83986e8 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 00:12:28 -0700 Subject: [PATCH 026/139] docs(waitable-queues): regenerate the published measurements with attribution The README and crate rustdoc carried a two-host comparison whose x64 sixteen- producer row was 193.5 / 52.2 -- the exact figures the probe's note flags as predating the correction to the timing window. Labelling them was a stopgap; this replaces them. **Re-measured**, on the machine this branch has been developed and verified on, with the capture parameters published beside the numbers rather than inferred: host banner verbatim, build profile, sampling parameters, run count, the instrument and its commit, and the date. A figure without those is not data anyone else can use, which is what D-observations-not-verdicts says and what the old table did not do. The table also gains the two columns it should always have had: `permit_mpsc`, which is the shape under evaluation, and `baseline_fetch_add`, which is N threads on one `AtomicU64` -- without it a reader cannot tell how much of any curve is the queue and how much is what this processor does to a contended line. **The ARM64 data point is gone rather than stale**, and that is stated plainly. Its figures predate the same correction, and neither the EPYC 7763 nor the Snapdragon X2 Elite is available here to retake them. Restoring a second architecture is what M2.15 in the probe crate's checklist is for. The one finding from that comparison that was structural rather than numeric is kept: the split was designed on the assumption that `slotwise_mpsc` would be cheaper, and measurement disagreed on both machines. Also in this pass, from the review: - **A stale section named a shape that never shipped.** `reserving_mpsc`'s module header still said a wide claim "ships instead as its own shape (`reserving_mpsc_wide`, not yet built)" and told callers to request 2^62 slots through it. D-41 made the wide word a layout in this module instead. Rewritten to say what shipped, keeping the reason the feature is non-default. - **"differing only in shift constants" was incomplete** -- `POSITION_MASK` is specialized per layout too, used by `advance` and `distance` and const-asserted to differ. `ClaimLayout`'s own rustdoc already said "shift and mask constants"; D-41 and the module header now agree with it. - **Two surviving client prescriptions**: "Measure your own workload before treating any of this as settled" in the README and its twin in the rustdoc. Replaced with what moves the numbers, and a statement that the probes exist and run the measurement -- which is a fact about the repository rather than an instruction. This also fixed a dangling reference, since "5.6x on one of these hosts" no longer had hosts to refer to. Verified: fmt and clippy clean; default-feature rustdoc clean (the module header's `Wide` mention deliberately does not link, since the type is `dwcas`-gated); 13 doctests including the compiled README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 93 ++++++++++++------- crates/windows-waitable-queues/src/lib.rs | 72 ++++++++------ .../src/reserving_mpsc.rs | 29 +++--- 4 files changed, 119 insertions(+), 77 deletions(-) diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 6401c07dd..c2fcb503c 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index f3734b873..28bb424bc 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -371,42 +371,65 @@ both rather than picking one for you. - **`spsc` requires exactly one producer and one consumer**, and does less work than either MPSC shape because of it. -The measurements below are what this workspace observed on the hosts named; they -are not a ranking, and which shape suits a given deployment is the deployment's -question. - -**These figures predate a correction to the probe's timing window and have not -been retaken.** The probe timed from the coordinator's clock rather than from the -producers' own, which overstated throughput, and the error grew with producer -count. The direction of the comparison survived re-measurement on the x64 host; -the absolute numbers here are optimistic and the high-producer rows most so. -Retaking them needs the two hosts named below, neither of which is the machine the -correction was measured on. See the queue-contention section of -[DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md). - -**What we measured**, in ns per push, isolated regime, median of three runs. -Higher producer counts oversubscribe both hosts: - -| producers | `slotwise_mpsc` (x64) | `reserving` (x64) | `slotwise_mpsc` (ARM64) | `reserving` (ARM64) | +The measurements below are one host's observation, recorded with the parameters +that produced them. They are not a ranking, and which shape suits a given +deployment is the deployment's question. + +**What was measured**, in ns per push, isolated regime (producers only, capacity +large enough that nothing is refused), median of three runs: + +| producers | `slotwise_mpsc` | `reserving_mpsc` | `permit_mpsc` | `baseline_fetch_add` | |---|---|---|---|---| -| 1 | 9.0 | 8.6 | 6.5 | 6.1 | -| 2 | 49.0 | 28.0 | 29.8 | 9.4 | -| 4 | 84.4 | 33.3 | 60.6 | 12.9 | -| 8 | 140.8 | 38.5 | 167.4 | 29.8 | -| 16 | 193.5 | 52.2 | 194.9 | 30.6 | -| 32 | 239.7 | 56.9 | 195.0 | 30.6 | - -x64 is an AMD EPYC 7763 slice (8 cores, 16 threads); ARM64 is a Snapdragon X2 -Elite (12 cores, no SMT). **Read these as two data points, not as a law.** This -comparison has already inverted once: it was designed on the assumption that -`slotwise_mpsc` would be the cheaper shape, and measurement said otherwise on both -machines. - -**Measure your own workload before treating any of this as settled.** Producer -count, how hard the consumer drains, and where the threads are scheduled all -move the answer -- thread placement alone moved an SPSC handoff by 5.6x on one -of these hosts. The `probe-core-affinity` tool in this repository exists so you -can run that measurement on your hardware instead of inheriting ours. +| 1 | 6.3 | 5.4 | 8.0 | 2.3 | +| 2 | 54.0 | 34.9 | 41.5 | 11.7 | +| 4 | 89.3 | 37.1 | 32.1 | 15.1 | +| 8 | 143.8 | 38.1 | 26.4 | 15.2 | +| 16 | 246.9 | 51.1 | 21.4 | 15.3 | +| 32 | 235.7 | 53.0 | 21.2 | 15.1 | + +**Attribution, because a figure without it is not reusable data:** + +| | | +|---|---| +| Host | `x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16]` | +| Profile | release | +| Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | +| Runs | 3 whole-probe invocations, median of the three | +| Instrument | `probe-queue-contention`, at commit `a99108f` | +| Taken | 2026-09-15 | + +The banner's `numa[16]` is a single NUMA node holding all sixteen processors, so +nothing here says anything about cross-domain behaviour. `permit_mpsc` is behind +`experimental-permit-claim` and is not covered by the semver promise. +`baseline_fetch_add` is N threads incrementing one `AtomicU64` -- the cheapest +thing N threads can do to a contended line, included so the queue figures can be +read against what this processor does to such a line at all. + +**Read these as one machine's numbers.** Producer counts above 8 oversubscribe +this host's 8 physical cores, and the spread across the three runs is not small: +`slotwise_mpsc` at sixteen producers gave 257.3, 215.1 and 246.9 across them. The +probe's own same-code control has been measured at 0.68-1.27x over seven runs, +which is wide enough to swallow small differences; see +[DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). + +**A previous version of this table compared two hosts** -- an AMD EPYC 7763 slice +and a Snapdragon X2 Elite -- and has been removed rather than carried forward. Its +figures predate a correction to the probe's timing window, which timed from the +coordinator's clock rather than the producers' own and overstated throughput by a +margin that grew with producer count; and neither of those machines is available +here to retake them. The ARM64 data point is therefore gone rather than stale, +which is the lesser of the two problems. Restoring one is what M2.15 in the +probe crate's [CHECKLIST.md](../windows-platform-probes/CHECKLIST.md) is for. + +That comparison did carry one finding worth keeping, because it was structural +rather than numeric: it was designed on the assumption that `slotwise_mpsc` would +be the cheaper shape, and measurement said otherwise on both machines. + +**What moves these numbers.** Producer count, how hard the consumer drains, and +where the threads are scheduled all change the answer -- thread placement alone +moved an SPSC handoff by 5.6x on an earlier host this workspace measured. The +`probe-core-affinity` tool in this repository runs that measurement, and +`probe-queue-contention` runs the one above. Two things that look like reasons to choose and are not: diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 13fdadbf6..32a52ee65 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -292,36 +292,54 @@ //! - **[`spsc`] requires exactly one producer and one consumer**, and does less //! work than either MPSC shape because of it. //! -//! The measurements below are what this workspace observed on the hosts named; -//! they are not a ranking. +//! The measurements below are one host's observation, recorded with the +//! parameters that produced them. They are not a ranking. //! -//! **They predate a correction to the probe's timing window and have not been -//! retaken.** The probe timed from the coordinator's clock rather than from the -//! producers' own, which overstated throughput, and the error grew with producer -//! count. The direction of the comparison survived re-measurement on the x64 -//! host; the absolute numbers here are optimistic, the high-producer rows most -//! so. Retaking them needs the two hosts named below, neither of which is the -//! machine the correction was measured on. +//! Isolated regime (producers only, capacity large enough that nothing is +//! refused), ns per push, median of three runs: //! -//! Measured ns per push, isolated regime, median of three. An AMD EPYC 7763 -//! slice (8 cores, 16 threads) and a Snapdragon X2 Elite (12 cores, no SMT): -//! -//! | producers | `slotwise_mpsc` x64 | `reserving` x64 | `slotwise_mpsc` ARM64 | `reserving` ARM64 | +//! | producers | `slotwise_mpsc` | `reserving_mpsc` | `permit_mpsc` | `baseline_fetch_add` | //! |---|---|---|---|---| -//! | 1 | 9.0 | 8.6 | 6.5 | 6.1 | -//! | 2 | 49.0 | 28.0 | 29.8 | 9.4 | -//! | 4 | 84.4 | 33.3 | 60.6 | 12.9 | -//! | 8 | 140.8 | 38.5 | 167.4 | 29.8 | -//! | 16 | 193.5 | 52.2 | 194.9 | 30.6 | -//! | 32 | 239.7 | 56.9 | 195.0 | 30.6 | -//! -//! **Read these as two data points, not as a law**, and measure your own -//! workload before treating them as settled. This comparison has already -//! inverted once: the split was designed on the assumption that `slotwise_mpsc` would be -//! the cheaper shape, and measurement disagreed on both machines. Producer -//! count, how hard the consumer drains, and where the threads are scheduled all -//! move the answer -- placement alone moved an SPSC handoff by 5.6x on one of -//! these hosts. +//! | 1 | 6.3 | 5.4 | 8.0 | 2.3 | +//! | 2 | 54.0 | 34.9 | 41.5 | 11.7 | +//! | 4 | 89.3 | 37.1 | 32.1 | 15.1 | +//! | 8 | 143.8 | 38.1 | 26.4 | 15.2 | +//! | 16 | 246.9 | 51.1 | 21.4 | 15.3 | +//! | 32 | 235.7 | 53.0 | 21.2 | 15.1 | +//! +//! Attribution, because a figure without it is not reusable data: +//! +//! | | | +//! |---|---| +//! | Host | `x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16]` | +//! | Profile | release | +//! | Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | +//! | Runs | 3 whole-probe invocations, median of the three | +//! | Instrument | `probe-queue-contention`, at commit `a99108f` | +//! | Taken | 2026-09-15 | +//! +//! The banner's `numa[16]` is a single NUMA node holding all sixteen processors, +//! so nothing here says anything about cross-domain behaviour. `permit_mpsc` is +//! behind `experimental-permit-claim` and is not covered by the semver promise. +//! `baseline_fetch_add` is N threads incrementing one `AtomicU64`, included so +//! the queue figures can be read against what this processor does to a contended +//! line at all. +//! +//! **Read these as one machine's numbers.** Producer counts above 8 oversubscribe +//! this host's 8 physical cores, and the spread across the three runs is not +//! small: `slotwise_mpsc` at sixteen producers gave 257.3, 215.1 and 246.9. +//! +//! A previous version of this table compared an AMD EPYC 7763 slice against a +//! Snapdragon X2 Elite. It was removed rather than carried forward: its figures +//! predate a correction to the probe's timing window, and neither machine is +//! available here to retake them. One finding from it was structural rather than +//! numeric and is worth keeping -- the split was designed on the assumption that +//! `slotwise_mpsc` would be the cheaper shape, and measurement disagreed on both +//! machines. +//! +//! **What moves these numbers.** Producer count, how hard the consumer drains, +//! and where the threads are scheduled -- placement alone moved an SPSC handoff +//! by 5.6x on an earlier host this workspace measured. //! //! Two things that look like reasons to choose and are not. **Capacity**: on a //! 64-bit target `slotwise_mpsc` reaches 2^62 slots and `reserving_mpsc` 2^31. diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index f3dd9e656..fe7614b08 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -28,7 +28,7 @@ //! **[`ClaimLayout`] is how far away that is.** [`Perpetual`] moves it to 2^56 //! pushes, about twenty years at the same rate, for the cost of a reservation //! ceiling of 255 -- it is the same exchange on -//! the same word, differing only in shift constants, though what that costs in +//! the same word, differing only in shift and mask constants, though what that costs in //! throughput is not established (see [`ClaimLayout`]). [`Enduring`] sits between //! them, and the `dwcas` feature adds a 128-bit word whose 64-bit position moves //! the recurrence to 2^64 pushes, which no deployment reaches. @@ -121,20 +121,21 @@ //! `2^(b-1)`, and the count needs `b` bits because it can reach the capacity, so //! `b + b = 64` gives `b = 32`. There is no cleverer division of the word. //! -//! **A 128-bit compare-and-swap is deliberately not used *here*** -//! ([D-37](../DESIGN-NOTES.md#d-37)). It would not remove the cost that -//! matters -- the consumer's position still has to be read -- and 2^31 slots is -//! a ring this shape allocates in full at construction. +//! **The 128-bit word ships as a layout, not as a separate shape** +//! ([D-37](../DESIGN-NOTES.md#d-37), amended by +//! [D-41](../DESIGN-NOTES.md#d-41)). An earlier plan put it in a shape of its own +//! called `reserving_mpsc_wide`; that shape was never built, and this paragraph +//! described it as forthcoming for longer than the plan survived. //! -//! The operative reason is that widening *this* shape's word would change what -//! it offers depending on the target: `i686-pc-windows-msvc` has no lock-free -//! 128-bit exchange, so the same module would be lock-free on one target and -//! silently mutex-backed on another. A wider claim ships instead as its own -//! shape (`reserving_mpsc_wide`, not yet built -- see D-37), to exist only -//! where the exchange is genuinely lock-free. That keeps *this* module's -//! contract the same on every target, which is the property being protected -//! here: a caller who wants 2^62 slots and no wrap hazard will ask for it by -//! name rather than get it by accident of where they compiled. +//! The reason for keeping it out of the default is unchanged: widening this +//! shape's word unconditionally would change what the module offers depending on +//! the target, because `i686-pc-windows-msvc` has no lock-free 128-bit exchange +//! and neither does an x86-64 build without `cmpxchg16b`. The same module would +//! be lock-free on one target and silently mutex-backed on another. So `Wide` +//! (which exists only under `dwcas`, so this names it without linking) is +//! reached by naming it, and the +//! narrow word's contract is identical on every target -- a caller gets the wide +//! one by asking, never by accident of where they compiled. use core::cell::{Cell, UnsafeCell}; use core::fmt; From d5628ceb7f59ea0257f8bbd1f84ce5d43f5287ab Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 12:55:34 -0400 Subject: [PATCH 027/139] docs: the reservation column was a field ceiling, not a reachable count The best finding in this round, and it corrects my own correction from bba97ba. Replacing `2^32` with `4,294,967,295` made the number exact but left the column label wrong, which was the actual defect: `MAX_RESERVED` is the ceiling of the packed count field, and admission is bounded by capacity as well -- `reserve` refuses once the ring has no room beyond the reservations already outstanding, so the achievable count is the lesser of the two. Which bound binds differs by layout, computed rather than asserted: Balanced field 4,294,967,295 | capacity 2^31 | achievable 2^31 <- CAPACITY Enduring field 65,535 | capacity 2^47 | achievable 65,535 <- field Perpetual field 255 | capacity 2^55 | achievable 255 <- field Wide field 4,294,967,295 | capacity 2^63 | achievable 4,294,967,295 <- field So the column was right for three layouts and wrong for the default, which is the one most callers get. All three tables now name it the reservation-count field ceiling and state the capacity bound beneath, calling out that `Balanced` is where capacity binds first. The same confusion sat in two more places: - The probe note's rollover table headed a column "max outstanding reservations" while listing field widths -- 2^32, 2^24, ... 2^64. A field of 2^n encodings holds a maximum count of 2^n - 1, and the shipping type caps the count at `u32::MAX` however wide the field is, so the 64/64 row's 2^64 buys no more reservations than 32/32's. Relabelled, with a note that the time columns are unaffected because they depend on the position half, which is what that table exists to compare. - D-41 said the reservation half "held 2^32". Now distinguishes the field width from the count the type will return and from what capacity permits. Also: the report's scaling table had a column headed `slotwise x1thr`, an abbreviation defined nowhere. Renamed to `slotwise`. My first attempt added an explanation above the table and a run showed it printing twice -- the text was already there below it -- so the fix is the label alone. Verified: fmt and clippy clean; 13 doctests including the compiled README; the duplicate-explanation regression caught by running the probe, not by reading it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/DESIGN-NOTES.md | 11 ++++++++++- .../src/bin/queue_contention.rs | 2 +- crates/windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 8 +++++++- crates/windows-waitable-queues/src/lib.rs | 7 ++++++- crates/windows-waitable-queues/src/reserving_mpsc.rs | 11 ++++++++++- 6 files changed, 35 insertions(+), 6 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 10c4a5ed6..099d1afb9 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -857,7 +857,7 @@ rate model reproduces the crate's own published figure -- 32/32 at 116M/s gives 37 seconds, which is what `reserving_mpsc`'s module documentation discloses -- so these are an extension of that disclosure rather than a competing estimate. -| split (reserved/position) | max outstanding reservations | @257M/s | @116M/s | @33M/s | +| split (reserved/position) | reservation field width | @257M/s | @116M/s | @33M/s | |---|---|---|---|---| | 32/32 (ships) | 2^32 | 17 s | 37 s | 2.2 min | | 24/40 | 2^24 | 71 min | 2.6 hr | 9.2 hr | @@ -868,6 +868,15 @@ these are an extension of that disclosure rather than a competing estimate. | 8/56 | 2^8 | 9 yr | 20 yr | 69 yr | | 64/64 (`u128`) | 2^64 | 2,270 yr | 5,039 yr | 17,607 yr | +**The second column is a field width, not a reachable reservation count**, and +the distinction matters twice. A field of 2^n encodings holds a maximum count of +2^n - 1; and the shipping type caps the count at `u32::MAX` however wide the +field is, because it is handed back to callers as a `u32` -- so the 64/64 row's +2^64 encodings buy no more reservations than 32/32's. Admission is bounded by +capacity as well, which for the shipping 32/32 layout binds first at 2^31 slots. +The time columns are unaffected: they depend on the *position* half, which is +what this table exists to compare. + Rates: 257M/s is the measured isolated peak at one producer, which has no consumer and so is not a rate any draining queue can sustain -- it is a conservative floor on time-to-wrap. 33M/s is the measured drained rate at one diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 91a9776e5..17c2f7925 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -117,7 +117,7 @@ fn render(out: &mut dyn std::fmt::Write) { let _ = writeln!( out, " {:<18} {:>12} {:>12} {:>12} {:>14}", - "producers", "slotwise x1thr", "reserving", "permit", "atomic floor" + "producers", "slotwise", "reserving", "permit", "atomic floor" ); for &producers in PRODUCER_COUNTS { let mpsc = observation.scaling(&observation.isolated, shapes::SLOTWISE_MPSC, producers); diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index c2fcb503c..267588bad 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: it held 2^32 where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: a 32-bit field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 28bb424bc..8ec631364 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -123,13 +123,19 @@ divided is now a caller's choice. Reservations are bounded by how many producers are mid-send -- hundreds at most -- so giving up a ceiling nobody reaches buys positions: -| Layout | Outstanding reservations | Pushes to recurrence | At sustained maximum rate | +| Layout | Reservation-count field ceiling | Pushes to recurrence | At sustained maximum rate | |---|---|---|---| | `Balanced` (default) | 4,294,967,295 | 2^32 | about 37 seconds | | `Enduring` | 65,535 | 2^48 | about 28 days | | `Perpetual` | 255 | 2^56 | about 20 years | | `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | +The first column is the field's ceiling, not a reachable number of reservations: +admission is also bounded by capacity -- `reserve` refuses once the ring has no +room beyond the reservations already outstanding -- so the achievable count is +the lesser of the two. For `Balanced` the capacity bound binds first, since that +layout accepts at most 2^31 slots. For the others the field binds. + ```rust use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 32a52ee65..f386fbf3d 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -91,13 +91,18 @@ //! many producers are mid-send -- hundreds at most -- so giving up a ceiling //! nobody reaches buys positions: //! -//! | Layout | Outstanding reservations | Pushes to recurrence | At sustained maximum rate | +//! | Layout | Reservation-count field ceiling | Pushes to recurrence | At sustained maximum rate | //! |---|---|---|---| //! | `Balanced` (default) | 4,294,967,295 | 2^32 | about 37 seconds | //! | `Enduring` | 65,535 | 2^48 | about 28 days | //! | `Perpetual` | 255 | 2^56 | about 20 years | //! | `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | //! +//! The first column is the field's ceiling, not a reachable number of +//! reservations: admission is also bounded by capacity, so the achievable count +//! is the lesser of the two. For `Balanced` the capacity bound binds first -- +//! that layout accepts at most 2^31 slots. For the others the field binds. +//! //! ``` //! use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; //! diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index fe7614b08..693311906 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -174,12 +174,21 @@ use crate::options::Options; /// hazard: a producer descheduled across a full wrap can claim against a /// numerically identical but generations-later value. /// -/// | Layout | reserved / position | Outstanding reservations | Pushes to recurrence | +/// | Layout | reserved / position | Reservation-count field ceiling | Pushes to recurrence | /// |---|---|---|---| /// | [`Balanced`] | 32 / 32 | 4,294,967,295 | 2^32 | /// | [`Enduring`] | 16 / 48 | 65,535 | 2^48 | /// | [`Perpetual`] | 8 / 56 | 255 | 2^56 | /// +/// **The middle column is the field's ceiling, not a reachable number of +/// reservations.** Admission is also bounded by capacity -- `reserve` refuses +/// once the ring has no room beyond the reservations already outstanding -- so +/// the achievable count is the lesser of the two. For [`Balanced`] the capacity +/// bound is the binding one: this layout accepts at most 2^31 slots, so no more +/// than 2^31 reservations can be outstanding whatever the field could hold. For +/// [`Enduring`] and [`Perpetual`] the field binds first, and the column is the +/// real limit. +/// /// At this crate's disclosed sustained rate of about 116 million pushes per /// second, those recurrences are roughly **37 seconds**, **28 days**, and /// **20 years** respectively. The rate is the one `reserving_mpsc`'s own hazard From c9a9127a45ad579edf379d01b132096f15857f23 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 13:24:20 -0400 Subject: [PATCH 028/139] docs: the refusal counts do not say what that section said they say Review found the last surviving site of the whole-path attribution defect; checking its evidence found that the evidence had inverted too. **The attribution, which is the reported finding.** The section closed by calling the isolated regime "the clean measurement of the claim itself", saying the drained regime shows the claim "is not the dominant cost", and placing a real application between the two. All three overstate: both regimes time the whole push path, the second rests on drained figures the same section had just shown to be confounded, and the third interpolates between regimes that measure different things, about deployments this crate has not seen. The paragraph contradicted the one four lines above it. **The evidence, which is worse.** That section is titled "the refusal counts say so" and rested on one comparison: 64/64 taking 12,149 refusals at eight producers against 32/32's 74,181, read as the slower shape being less backpressured. Re-measured three times on the same host, the counts neither reproduce nor keep their order: run 1: 32/32 = 925 64/64 = 14,461 run 2: 32/32 = 12,613 64/64 = 2,152 run 3: 32/32 = 1,814 64/64 = 10,337 The ordering reverses between runs and the magnitudes span more than an order of magnitude either way, so no single run establishes which shape was more backpressured. The original numbers were one run. The mechanism is still sound -- a slower producer is less backpressured, and refusal retries sit inside the timed region -- so the section keeps it as a *confound*: drained figures contain retry time of unknown and varying size, which is a reason to distrust drained ratios. That is weaker than "the refusal counts say so", and it is what the data supports. The section is retitled accordingly. Also swept: the queue crate's own note captioned its isolated table "the cleanest measurement of the claim", the same attribution in the other crate. Now "the cleanest comparison between the shapes ... still a measurement of each shape's whole push path, not of the claim alone". Verified: three probe runs; encoding and ASCII clean; no inbound links to the retitled heading; 13 doctests including the compiled README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 51 ++++++++++++++----- .../windows-waitable-queues/DESIGN-NOTES.md | 3 +- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 099d1afb9..f9c8a9739 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -817,20 +817,47 @@ same-code control spanning -32% to +27%. A number that agrees with its predecessor is not thereby established; that is precisely the trap the control exists to catch, and this is the case where it catches it. -### The drained regime flatters the slower layout, and the refusal counts say so +### The drained regime is hard to read, and the refusal counts do not settle it The two regimes must not be averaged, and the drained one must not be read as -the answer on its own. **A slower producer is less backpressured**, so it earns -fewer refusals, and refusal retries are inside the timed region. At eight -producers the 64/64 layout took 12,149 refusals against 32/32's 74,181 -- so -part of what makes its per-push number look close is that it spent less time -being turned away. The drained figures are therefore an *understatement* of the -128-bit word's cost, not a measurement of it under load. - -The isolated regime is the clean measurement of the claim itself; the drained -one shows that in a queue doing real work the claim is not the dominant cost. A -real application sits between them, nearer the drained end the more -consumer-bound it is. +the answer on its own. The mechanism is structural: **a slower producer is less +backpressured**, so it earns fewer refusals, and refusal retries are inside the +timed region -- which means part of what makes a slower shape's per-push number +look close may be that it spent less time being turned away. + +**The refusal counts were offered here as evidence of that, and they do not +support it.** An earlier version of this section reported the 64/64 layout taking +12,149 refusals at eight producers against 32/32's 74,181, and read the +asymmetry as the mechanism showing through. Re-measured three times on the same +host, the counts are neither stable nor consistently ordered: + +| run | 32/32 refusals | 64/64 refusals | +|---|---|---| +| 1 | 925 | 14,461 | +| 2 | 12,613 | 2,152 | +| 3 | 1,814 | 10,337 | + +The ordering reverses between runs and the magnitudes span more than an order of +magnitude either way, so no single run's counts establish anything about which +shape was more backpressured. The original figures were one run, and they are not +reproducible in direction or in size. + +What survives is the confound, not a measurement of it: the drained numbers +contain retry time whose amount is unknown and varies between runs, so a drained +difference cannot be read as a difference in push cost. That is a reason to +distrust the drained ratios, which is weaker than the claim this section +previously made and is what the data supports. + +**The isolated regime removes consumer traffic; it does not isolate the claim.** +Both regimes time the whole push path -- the tail claim, the slot-sequence load, +the item write, the publication store and the doorbell's fence. An earlier +version of this paragraph called the isolated regime "the clean measurement of +the claim itself", said the drained one shows the claim "is not the dominant +cost", and placed a real application between the two. None of the three follows: +the first attributes a whole-path number to one operation, the second rests on +drained figures the paragraph above has just shown to be confounded, and the +third is an interpolation between two regimes that measure different things, +offered about deployments this crate has not seen. ### What the control caught diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 267588bad..eed82abb7 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -1283,7 +1283,8 @@ most so. ### Isolated regime -- producers only, nothing ever refused -The cleanest measurement of the claim, because nothing else touches the queue. Nanoseconds per push: +The cleanest comparison between the shapes, because nothing else touches the queue -- but still a +measurement of each shape's whole push path, not of the claim alone. Nanoseconds per push: | producers | `slotwise_mpsc` | `reserving_mpsc` | `permit_mpsc` | contended `fetch_add` | |---|---|---|---|---| From 8b3165013477ec608fb122f6c1a290cabd274746 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 15:30:21 -0400 Subject: [PATCH 029/139] docs: mark D-17 superseded, and scope the forced-split premise to Balanced The substantive finding is a decision record contradicting the contract this branch publishes. **D-17 was not marked superseded.** Its section still reads "The 32/32 split is forced, not chosen", derives it from "the count can reach the capacity, so it needs `b` bits too", and gives 2^31 as *the shape's* ceiling. D-41 dropped exactly that premise -- capping outstanding reservations well below capacity is what frees the position to take 48 or 56 bits, which is where `Enduring` and `Perpetual` come from. The repository rule is explicit that a superseded decision carries a status marker adjacent to its title, and this one had none, so the decision record and the layout contract disagreed with nothing flagging it. D-17 now carries a "Partly superseded by D-41" marker and the derivation is scoped: forced *given that premise*, with the 2^31 ceiling identified as `Balanced`'s. The arithmetic is untouched -- D-41 removed the requirement, not the algebra. The same premise sat in `reserving_mpsc`'s module header, telling callers the split is "forced rather than chosen" a few paragraphs above the layouts that disprove it. Scoped the same way, with a pointer to what `ClaimLayout` changed. Three more, all mine and all from sweeps that stopped short: - **Two surviving "`Wide` removes it"** claims, in the README and crate rustdoc hazard bullets, against tables in the same documents saying the position recurs after 2^64 pushes. The previous round corrected five sites of this and missed these two because they are phrased as a bare pronoun. - **`Balanced`'s rustdoc said it "Holds `u32::MAX` outstanding reservations"** -- the field ceiling, which its own capacity of 2^31 slots makes unreachable. The tables were corrected for this last round; the prose beside them was not. - **The explanatory sentence under two tables said "the first column"** when the first column is `Layout` and the ceiling is the second. Now names the column. And one I had judged the other way in an earlier round: the probe note's "the candidates worth considering are 12/52 and 8/56" is a layout recommendation, which `D-no-client-prescriptions` forbids. I had called it internal analysis and left it. It reads as a recommendation because it is one; the arithmetic that separates the rows is kept and the choice handed back. Verified: fmt and clippy clean; default-feature rustdoc clean; 13 doctests including the compiled README; claim sweeps for all three corrections return empty. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 8 +++-- .../windows-waitable-queues/DESIGN-NOTES.md | 18 ++++++++--- crates/windows-waitable-queues/README.md | 4 +-- crates/windows-waitable-queues/src/lib.rs | 4 +-- .../src/reserving_mpsc.rs | 31 +++++++++++++------ 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index f9c8a9739..016aef39d 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -927,9 +927,11 @@ and a rate, not from a measurement of either layout; what does not follow is any statement about what the re-apportionment costs to run. See [Re-measured on the shipping type](#d-queue-layout-observations). -So the candidates worth considering are **12/52 and 8/56**, not the 16/48 first -sketched here: 16/48's 12.7 days at the conservative floor is still reachable by -a busy long-lived process, and 12/52 is the first row that is not. +So the arithmetic separates the rows this way: 16/48's 12.7 days at the +conservative floor is still reachable by a busy long-lived process, and 12/52 is +the first row that is not. Which of them a caller wants is the caller's question, +and the shipping type takes the layout as a parameter so it stays theirs -- see +[D-no-client-prescriptions](#d-no-client-prescriptions). ### Re-measured on the shipping type, with the probe's own control to read it against diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index eed82abb7..02005b32f 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -541,6 +541,13 @@ M31.5 rather than as an intention here. ## D-17: the reservation count and the claim position share one word +**Partly superseded by [D-41](#d-41): the 32/32 split is no longer forced.** The packing argument below +is unchanged and still describes `Balanced`, but its premise -- that the count must be able to reach the +whole capacity -- was the thing D-41 dropped. Capping outstanding reservations instead frees the position +to take 48 or 56 bits, so the split became a caller-selected layout rather than the only division of the +word. Read "forced" below as "forced *given that premise*", and the 2^31 ceiling as `Balanced`'s rather +than the shape's. + **The obvious implementation is broken, and it is worth writing down why, because the brokenness is not visible from reading either side on its own.** With the count in its own atomic: @@ -571,10 +578,13 @@ Three consequences fall out, and all three are improvements: consumer's position anyway. So `reserving_mpsc`'s `pop` is one store shorter than `slotwise_mpsc`'s: nothing writes a "free again" sequence. -**The 32/32 split is forced, not chosen.** A position of `b` bits keeps a wrapping difference unambiguous -only up to `2^(b-1)`; the count can reach the capacity, so it needs `b` bits too; `b + b = 64` gives -`b = 32`. There is no cleverer division of the word, and the resulting ceiling is 2^31 items -- a ring -this shape allocates in full at construction, so at eight bytes an item it is already 17 GB. +**The 32/32 split is forced by the premise above, which [D-41](#d-41) later dropped.** A position of `b` bits +keeps a wrapping difference unambiguous +only up to `2^(b-1)`; if the count must be able to reach the capacity it needs `b` bits too; `b + b = 64` gives +`b = 32`. Given that requirement there is no cleverer division of the word, and the resulting ceiling is 2^31 items -- a ring +this shape allocates in full at construction, so at eight bytes an item it is already 17 GB. D-41 removed the +requirement rather than the arithmetic: capping outstanding reservations well below capacity lets the position +take 48 or 56 bits, which is what `Enduring` and `Perpetual` do. The 2^31 ceiling is therefore `Balanced`'s. That ceiling is reported through `CapacityError`'s `max_valid`, which [D-12](#d-12) had already made a property of the shape rather than of the crate. D-12 introduced that for the *minimum* and argued the diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 8ec631364..c6f53a304 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -130,7 +130,7 @@ positions: | `Perpetual` | 255 | 2^56 | about 20 years | | `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | -The first column is the field's ceiling, not a reachable number of reservations: +The middle column is the field's ceiling, not a reachable number of reservations: admission is also bounded by capacity -- `reserve` refuses once the ring has no room beyond the reservations already outstanding -- so the achievable count is the lesser of the two. For `Balanced` the capacity bound binds first, since that @@ -161,7 +161,7 @@ The default remains `Balanced` so that no existing caller's behaviour changed when the choice was introduced. Under it, a queue driven past 2^32 pushes by two or more producers can **silently lose an item** -- the defect described above. `Enduring` and `Perpetual` move that point out by 2^16 and 2^24 respectively, and -`Wide` removes it. +`Wide` moves it to 2^64 pushes. **What happens.** A producer checks that there is room, is descheduled, and resumes after other producers have driven the position field through a complete diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index f386fbf3d..2b7bae486 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -98,7 +98,7 @@ //! | `Perpetual` | 255 | 2^56 | about 20 years | //! | `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | //! -//! The first column is the field's ceiling, not a reachable number of +//! The reservation-count column is the field's ceiling, not a reachable number of //! reservations: admission is also bounded by capacity, so the achievable count //! is the lesser of the two. For `Balanced` the capacity bound binds first -- //! that layout accepts at most 2^31 slots. For the others the field binds. @@ -132,7 +132,7 @@ //! changed when the choice was introduced. Under it, a queue driven past 2^32 //! pushes by two or more producers can **silently lose an item** -- the defect //! described above. `Enduring` and `Perpetual` move that point out, and `Wide` -//! removes it. +//! moves it to 2^64 pushes. //! //! **What happens.** A producer checks that there is room, is descheduled, and //! resumes after other producers have driven the position field through a diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 693311906..85ee07649 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -115,11 +115,19 @@ //! //! # What the packing costs, and what it does not //! -//! Splitting a 64-bit word 32/32 caps this shape at -//! a maximum of 2^31 items, and that split is forced rather than chosen: +//! Splitting a 64-bit word 32/32 caps `Balanced` at +//! a maximum of 2^31 items, and that split is forced *given one premise*: //! a position of `b` bits keeps a wrapping difference unambiguous only up to -//! `2^(b-1)`, and the count needs `b` bits because it can reach the capacity, so -//! `b + b = 64` gives `b = 32`. There is no cleverer division of the word. +//! `2^(b-1)`, and if the count must be able to reach the capacity it needs `b` +//! bits too, so `b + b = 64` gives `b = 32`. Given that requirement there is no +//! cleverer division of the word. +//! +//! **[`ClaimLayout`] drops the requirement rather than the arithmetic.** Capping +//! outstanding reservations well below capacity -- 65,535 under [`Enduring`], 255 +//! under [`Perpetual`] -- frees the position to take 48 or 56 bits, which is +//! where the deeper layouts come from. So the derivation above describes +//! [`Balanced`] and the capacity-filling case, not every layout this module +//! offers. //! //! **The 128-bit word ships as a layout, not as a separate shape** //! ([D-37](../DESIGN-NOTES.md#d-37), amended by @@ -485,17 +493,22 @@ impl ClaimWord for u128 { /// The shipping division: 32 bits each. /// -/// Holds [`u32::MAX`] outstanding reservations and recurs after 2^32 pushes -- +/// Its reservation-count field tops out at [`u32::MAX`], though capacity binds +/// first: this layout accepts at most 2^31 slots, so no instance can hold more +/// than that many outstanding reservations whatever the field could encode. It +/// recurs after 2^32 pushes -- /// about /// **37 seconds** of sustained maximum-rate pushing. Past that point, with two /// or more producers, the queue can **silently lose an item**: that is the whole /// of the `SH-14.1` exposure, and this layout carries it. /// /// It is the default because it is what the shape shipped with, not because the -/// division is a good one: the [`u32::MAX`] reservation ceiling it buys is far -/// beyond any use this crate has seen, and the exposure is what pays for it. -/// [`Enduring`] and [`Perpetual`] spend that ceiling the other way -- -/// [`Enduring`] holds 65,535 outstanding reservations, [`Perpetual`] 255 -- +/// division is a good one: the reservation field it buys is far +/// beyond any use this crate has seen -- and beyond what its own capacity +/// permits -- while the exposure is what pays for it. +/// [`Enduring`] and [`Perpetual`] spend that field the other way -- +/// [`Enduring`] holds 65,535 outstanding reservations, [`Perpetual`] 255, both +/// reachable because capacity does not bind there -- /// and `Wide` moves it to 2^64 pushes rather than to a horizon in years. (`Wide` exists /// only under the `dwcas` feature, so this names it without linking: an /// intra-doc link here would not resolve in a default-feature rustdoc build.) From 16c329ea7e074bd59e756d22a01dfb4fa10da35a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 15:51:07 -0400 Subject: [PATCH 030/139] docs: the 2^31 capacity is a 64-bit figure, and other target-dependence Five of the eight findings were one error of mine: I stated `Balanced`'s capacity ceiling as 2^31 with no target qualification, in five places. `BOUNDS_MAX` clamps to `MAX_ADMISSIBLE_CAPACITY`, which is `1 << (usize::BITS - 2)` -- 2^62 on a 64-bit target but **2^30 on a 32-bit one**, where it binds before the layout's own 2^31. So on i686 every layout stops at 2^30, and my sentences contradicted this crate's own capacity section, which has always said so. Verified rather than reasoned, because the reviewer's question was whether the bound is 2^30 or 2^30 - 1. It is exactly 2^30: capacity must be a power of two (ring masking) and the bound is inclusive -- `validate_capacity` debug-asserts `bounds.max.is_power_of_two()` precisely because the maximum is handed to callers as the value to retry with. Asked the code directly, on 64-bit: Balanced max_valid = 2,147,483,648 = 2^31 Enduring max_valid = 140,737,488,355,328 = 2^47 Perpetual max_valid = 36,028,797,018,963,968 = 2^55 all powers of two, matching the computed table. This is the opposite convention to `MAX_RESERVED`, which IS `2^n - 1`, because that is the largest *value* a field can hold and zero is legal; a capacity is a count of slots constrained to powers of two, with a separate minimum excluding zero. Also: - **The layout ratio table carried no regime label** while the paragraph beneath it discusses the drained regime. Its values are isolated -- 64/64 at 1.37x and 3.45x -- and the drained text immediately below says those same layouts sit inside the control band. A reader could attribute one regime's figures to the other. Columns now say isolated, with a note that mixing them is the misreading the label prevents. - **`ClaimLayout`'s table lists three layouts and there are four** when `dwcas` is on. Now says so, naming `Wide` without linking (it does not exist in a default build, which is the same constraint that produced an unresolved intra-doc link two rounds ago). - **D-18's supersedence marker described a shape that was never built.** It said D-37 adopted the wide word "for a separate wide shape", with `reserving_mpsc_wide` "a peer beside it" -- D-37's original plan, which D-41 replaced with a layout inside `reserving_mpsc`. The one-hop pointer to D-37 is correct and stays; the description of what D-37 did is corrected, and the retired name marked as history. Verified: fmt and clippy clean; default-feature rustdoc clean; 13 doctests including the compiled README. The scratch test used to interrogate the capacity bounds was removed and its `mod` declaration with it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/DESIGN-NOTES.md | 6 +++++- crates/windows-waitable-queues/DESIGN-NOTES.md | 9 ++++++--- crates/windows-waitable-queues/README.md | 3 ++- crates/windows-waitable-queues/src/lib.rs | 3 ++- .../src/reserving_mpsc.rs | 18 +++++++++++++----- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 016aef39d..362114e62 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1010,7 +1010,11 @@ What follows is therefore reported as *data with a known-unexplained spread*, which is a reasonable input for planning a deployment on comparable hardware and an unreasonable basis for a comparative claim about the layouts. -| producers | 16/48 vs 32/32 | 8/56 vs 32/32 | 64/64 vs 32/32 | +**Isolated regime**, median of the per-run ratios with the observed range beside +it. The drained regime is reported in the paragraph below the table, and mixing +the two is the reading this label exists to prevent: + +| producers | 16/48 vs 32/32 (isolated) | 8/56 vs 32/32 (isolated) | 64/64 vs 32/32 (isolated) | |---|---|---|---| | 1 | 1.00x [0.74-1.00] | 1.00x [0.67-1.04] | 1.37x [1.16-1.57] | | 2 | 0.94x [0.89-1.05] | 0.96x [0.80-0.98] | 1.13x [1.02-1.15] | diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 02005b32f..f6c43d0a2 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -599,9 +599,12 @@ constraint that binds: the count's half must be wide enough to hold the whole ca ## D-18: a 128-bit compare-and-swap is refused -**Superseded by [D-37](#d-37).** A 128-bit exchange is now adopted, but for a **separate wide -shape** rather than for this one: `reserving_mpsc` keeps its packed 64-bit word on every target, and -`reserving_mpsc_wide` is a peer beside it. Read this decision for the cost analysis, which D-37 +**Superseded by [D-37](#d-37).** A 128-bit exchange is now adopted, though not in the form D-37 first +proposed: it planned a **separate wide shape**, `reserving_mpsc_wide`, as a peer beside this one, and +[D-41](#d-41) replaced that with a `ClaimLayout` inside `reserving_mpsc`. The peer shape was never +built, so the name appears in this file only as history. What survives unchanged is that +`reserving_mpsc` keeps its packed 64-bit word on every target and the wide word is reached by asking +for it. Read this decision for the cost analysis, which D-37 depends on and does not repeat -- and note one correction it needs, below, that D-37's gate is built around. diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index c6f53a304..ffaf33293 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -134,7 +134,8 @@ The middle column is the field's ceiling, not a reachable number of reservations admission is also bounded by capacity -- `reserve` refuses once the ring has no room beyond the reservations already outstanding -- so the achievable count is the lesser of the two. For `Balanced` the capacity bound binds first, since that -layout accepts at most 2^31 slots. For the others the field binds. +layout accepts at most 2^31 slots on a 64-bit target, and 2^30 on a 32-bit +one. For the others the field binds on either. ```rust use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 2b7bae486..54e5d4281 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -101,7 +101,8 @@ //! The reservation-count column is the field's ceiling, not a reachable number of //! reservations: admission is also bounded by capacity, so the achievable count //! is the lesser of the two. For `Balanced` the capacity bound binds first -- -//! that layout accepts at most 2^31 slots. For the others the field binds. +//! that layout accepts at most 2^31 slots on a 64-bit target, and 2^30 on a +//! 32-bit one. For the others the field binds on either. //! //! ``` //! use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 85ee07649..7a73f7f7f 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -116,7 +116,9 @@ //! # What the packing costs, and what it does not //! //! Splitting a 64-bit word 32/32 caps `Balanced` at -//! a maximum of 2^31 items, and that split is forced *given one premise*: +//! a maximum of 2^31 items on a 64-bit target -- 2^30 on a 32-bit one, where +//! the crate-wide ceiling binds first -- and that split is forced *given one +//! premise*: //! a position of `b` bits keeps a wrapping difference unambiguous only up to //! `2^(b-1)`, and if the count must be able to reach the capacity it needs `b` //! bits too, so `b + b = 64` gives `b = 32`. Given that requirement there is no @@ -188,12 +190,17 @@ use crate::options::Options; /// | [`Enduring`] | 16 / 48 | 65,535 | 2^48 | /// | [`Perpetual`] | 8 / 56 | 255 | 2^56 | /// +/// A fourth layout, `Wide` (64 / 64 over a `u128`), exists when the `dwcas` +/// feature is enabled; it is omitted from this table because it does not exist +/// in a default build, and named without a link here for the same reason. +/// /// **The middle column is the field's ceiling, not a reachable number of /// reservations.** Admission is also bounded by capacity -- `reserve` refuses /// once the ring has no room beyond the reservations already outstanding -- so /// the achievable count is the lesser of the two. For [`Balanced`] the capacity -/// bound is the binding one: this layout accepts at most 2^31 slots, so no more -/// than 2^31 reservations can be outstanding whatever the field could hold. For +/// bound is the binding one: this layout accepts at most 2^31 slots on a 64-bit +/// target and 2^30 on a 32-bit one, so no more than that many reservations can +/// be outstanding whatever the field could hold. For /// [`Enduring`] and [`Perpetual`] the field binds first, and the column is the /// real limit. /// @@ -494,8 +501,9 @@ impl ClaimWord for u128 { /// The shipping division: 32 bits each. /// /// Its reservation-count field tops out at [`u32::MAX`], though capacity binds -/// first: this layout accepts at most 2^31 slots, so no instance can hold more -/// than that many outstanding reservations whatever the field could encode. It +/// first: this layout accepts at most 2^31 slots on a 64-bit target and 2^30 on +/// a 32-bit one, so no instance can hold more than that many outstanding +/// reservations whatever the field could encode. It /// recurs after 2^32 pushes -- /// about /// **37 seconds** of sustained maximum-rate pushing. Past that point, with two From 34b47d6d431e2be4f9a9c742a05ef04113cfd6b7 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 16:24:55 -0400 Subject: [PATCH 031/139] test(platform-probes): cover measured_span and the report's renderer The timing correction this branch made in review round two -- replacing the coordinator's clock with a span of min(start)..max(end) over the workers' own timestamps -- shipped without a test. It is the probe's core correctness invariant and the place the original defect lived, so four unit tests now pin it: the span runs from the earliest start to the latest finish, it is never narrower than any single worker, a lone worker is its own span, and disjoint workers still yield one covering window. Built from constructed Instants, so they are deterministic and cost nothing. The report's four rendering helpers lived in the binary, where no test could reach them. tests.rs consequently *documented* the non-finite scaling case without checking it, and format_ratio's zero-denominator guard was unreachable by the gate. Moved render_table, format_scaling, format_ratio and format_nanos into the library alongside the measurement they render -- which is also where the crate's "a probe is a function that returns an observation, never a program that prints one" decision puts them -- and covered missing, zero and non-finite rows. Both guards verified by sabotage rather than by reading: measured_span: min -> max on the start fails three of the four tests (the single-worker case cannot fail, since min == max there). format_ratio: dropping the `> 0.0` guard renders "infx" and fails. Release report re-rendered after the move and is unchanged in structure. Also closes two documentation findings from the same round: - D-17's decision-index row carried no supersedence marker, though its section did. A reader following the index got the withdrawn premise -- that the count must reach the whole capacity -- presented as current. The row now carries the marker and states the capacity ceiling as Balanced's, target-aware: 2^31 on 64-bit, 2^30 on 32-bit. - The README's attributed three-run table and the seven-run variance sweep are two separate captures of the same probe and host, and the README cited the sweep without saying so. It now says which is which, and that their disagreement is the sweep's finding rather than an inconsistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 62 +------ .../src/queue_contention.rs | 66 ++++++++ .../src/queue_contention/tests.rs | 157 ++++++++++++++++++ .../windows-waitable-queues/DESIGN-NOTES.md | 5 +- crates/windows-waitable-queues/README.md | 5 + 5 files changed, 233 insertions(+), 62 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 17c2f7925..96754a525 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -13,7 +13,8 @@ //! cannot separate. use windows_platform_probes::queue_contention::{ - DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, Run, measure, shapes, + DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, format_nanos, + format_ratio, format_scaling, measure, render_table, shapes, }; use windows_platform_probes::report::emit_report; @@ -431,62 +432,3 @@ fn render(out: &mut dyn std::fmt::Write) { " many refusals was waiting for the consumer, not for the tail." ); } - -/// Append one regime's table to `out`. -/// -/// Takes the buffer rather than printing, for the reason `pool_growth`'s twin -/// records: a helper writing to stdout while its caller composes a string emits -/// its lines first, reordering the report without losing any of it. -fn render_table(out: &mut dyn std::fmt::Write, runs: &[Run]) { - let _ = writeln!( - out, - "{:<18} {:>10} {:>14} {:>16} {:>14}", - "shape", "producers", "ns/push", "pushes/sec", "refusals" - ); - for run in runs { - let _ = writeln!( - out, - "{:<18} {:>10} {:>14.1} {:>16.0} {:>14}", - run.shape, run.producers, run.nanos_per_push, run.pushes_per_second, run.refusals - ); - } -} - -/// A scaling factor, or `--` when it is missing or not a number. -/// -/// Guards non-finite values for the same reason [`format_ratio`] guards its -/// denominator, and the guard belongs here rather than in `scaling`: a shape -/// whose one-producer row reports zero makes the quotient infinite, and -/// `infx` in a column of measurements reads as a measurement. `scaling` is -/// deliberately allowed to return the non-finite value -- it is arithmetic, not -/// a renderer -- so the display layer is where it has to be caught. -fn format_scaling(scaling: Option) -> String { - match scaling { - Some(value) if value.is_finite() => format!("{value:.2}x"), - _ => "--".to_owned(), - } -} - -/// `numerator / denominator` as a cost ratio, or `--` when either is missing. -/// -/// Guards the denominator rather than trusting it: a shape that failed to run -/// reports zero, and a division by it would print `inf` or `NaN` in a column a -/// reader would otherwise take for a measurement. -fn format_ratio(numerator: Option, denominator: Option) -> String { - match (numerator, denominator) { - (Some(numerator), Some(denominator)) if denominator.nanos_per_push > 0.0 => { - format!( - "{:.2}x", - numerator.nanos_per_push / denominator.nanos_per_push - ) - } - _ => "--".to_owned(), - } -} - -fn format_nanos(run: Option) -> String { - run.map_or_else( - || "--".to_owned(), - |run| format!("{:.1}", run.nanos_per_push), - ) -} diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 1d996ee84..228c06649 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -76,6 +76,7 @@ //! scaling with producer count, and the drained one for the end-to-end shape //! comparison taken while `head` is being written. +use std::fmt; use std::sync::Arc; use std::sync::Barrier; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; @@ -221,6 +222,71 @@ impl Observation { } } +/// Renders one regime's rows as the report's table body. +/// +/// Here rather than in the binary so it can be tested without running the +/// measurement. It takes a sink rather than writing to stdout for the reason +/// [`crate::report`] records: a helper writing to stdout while its caller +/// composes a string emits its lines first, reordering the report without losing +/// any of it. +pub fn render_table(out: &mut dyn fmt::Write, runs: &[Run]) { + let _ = writeln!( + out, + "{:<18} {:>10} {:>14} {:>16} {:>14}", + "shape", "producers", "ns/push", "pushes/sec", "refusals" + ); + for run in runs { + let _ = writeln!( + out, + "{:<18} {:>10} {:>14.1} {:>16.0} {:>14}", + run.shape, run.producers, run.nanos_per_push, run.pushes_per_second, run.refusals + ); + } +} + +/// A scaling factor, or `--` when it is missing or not a number. +/// +/// Guards non-finite values for the same reason [`format_ratio`] guards its +/// denominator, and the guard belongs here rather than in [`Observation::scaling`]: +/// a shape whose one-producer row reports zero makes the quotient infinite, and +/// `infx` in a column of measurements reads as a measurement. `scaling` is +/// deliberately allowed to return the non-finite value -- it is arithmetic, not +/// a renderer -- so the display layer is where it has to be caught. +#[must_use] +pub fn format_scaling(scaling: Option) -> String { + match scaling { + Some(value) if value.is_finite() => format!("{value:.2}x"), + _ => "--".to_owned(), + } +} + +/// `numerator / denominator` as a cost ratio, or `--` when either is missing. +/// +/// Guards the denominator rather than trusting it: a shape that failed to run +/// reports zero, and a division by it would print `inf` or `NaN` in a column a +/// reader would otherwise take for a measurement. +#[must_use] +pub fn format_ratio(numerator: Option, denominator: Option) -> String { + match (numerator, denominator) { + (Some(numerator), Some(denominator)) if denominator.nanos_per_push > 0.0 => { + format!( + "{:.2}x", + numerator.nanos_per_push / denominator.nanos_per_push + ) + } + _ => "--".to_owned(), + } +} + +/// One row's nanoseconds per push, or `--` when the row is missing. +#[must_use] +pub fn format_nanos(run: Option) -> String { + run.map_or_else( + || "--".to_owned(), + |run| format!("{:.1}", run.nanos_per_push), + ) +} + /// Time every configuration. #[must_use] pub fn measure() -> Observation { diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index ce7268ffc..b22f4dabf 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -6,6 +6,7 @@ //! testing directly; none of them needs the 65-second measurement. use super::*; +use std::time::Duration; /// A `Run` with everything but the fields under test held constant. fn run(shape: &'static str, producers: usize, pushes_per_second: f64) -> Run { @@ -271,3 +272,159 @@ fn capacity_for_leaves_room_for_every_push_at_every_producer_count() { ); } } + +/// `measured_span` is the correction that round two of this branch's review +/// produced, and it had no test until round seventeen asked for one. The probe +/// previously timed from the coordinator's clock, which understated elapsed time +/// and overstated throughput by roughly 45% at high producer counts. These pin +/// the shape of the replacement: the span runs from the EARLIEST worker start to +/// the LATEST worker finish, so no worker's time is outside it. +/// +/// `Instant` cannot be constructed from a literal, so each case builds one from +/// a single `now` and offsets it. That keeps the arithmetic exact without making +/// the test depend on how long it takes to run. +#[test] +fn measured_span_runs_from_the_earliest_start_to_the_latest_finish() { + let base = Instant::now(); + let ms = Duration::from_millis(1); + // Three workers, deliberately out of order and overlapping: the earliest + // start belongs to the second, the latest finish to the third. + let spans = vec![ + (base + 10 * ms, base + 40 * ms), + (base + 5 * ms, base + 20 * ms), + (base + 30 * ms, base + 60 * ms), + ]; + let nanos = measured_span(&spans); + // 5ms..60ms + let expected = (55 * ms).as_nanos() as f64; + assert!( + (nanos - expected).abs() < f64::EPSILON, + "expected {expected} ns, got {nanos}" + ); +} + +/// The defect the correction replaced would have measured one worker's slice, or +/// the coordinator's view of it. Any narrower aggregation than min-start to +/// max-end is therefore what this guards against. +#[test] +fn measured_span_is_wider_than_any_single_worker() { + let base = Instant::now(); + let ms = Duration::from_millis(1); + let spans = vec![ + (base + 10 * ms, base + 20 * ms), + (base + 15 * ms, base + 50 * ms), + (base, base + 5 * ms), + ]; + let nanos = measured_span(&spans); + for (began, ended) in &spans { + let worker = ended.duration_since(*began).as_nanos() as f64; + assert!( + nanos >= worker, + "span {nanos} must cover every worker, but one ran {worker}" + ); + } + assert!( + (nanos - (50 * ms).as_nanos() as f64).abs() < f64::EPSILON, + "expected the full 0..50ms window, got {nanos}" + ); +} + +#[test] +fn measured_span_of_one_worker_is_that_worker() { + let base = Instant::now(); + let ms = Duration::from_millis(1); + let nanos = measured_span(&[(base + 3 * ms, base + 11 * ms)]); + assert!( + (nanos - (8 * ms).as_nanos() as f64).abs() < f64::EPSILON, + "expected 8ms, got {nanos}" + ); +} + +/// Workers that never overlap still yield one span covering both, because the +/// question the probe asks is how long the whole configuration took. +#[test] +fn measured_span_covers_disjoint_workers() { + let base = Instant::now(); + let ms = Duration::from_millis(1); + let nanos = measured_span(&[(base, base + ms), (base + 100 * ms, base + 101 * ms)]); + assert!( + (nanos - (101 * ms).as_nanos() as f64).abs() < f64::EPSILON, + "expected 101ms, got {nanos}" + ); +} + +/// The renderer's cells. These were in the binary and therefore untestable until +/// they moved into this module; the non-finite case in particular is documented +/// by `scaling_against_a_zero_rate_is_non_finite_rather_than_a_panic` above and +/// was relying on that documentation rather than on a check. +#[test] +fn format_scaling_renders_a_finite_value_and_marks_everything_else() { + assert_eq!(format_scaling(Some(1.0)), "1.00x"); + assert_eq!(format_scaling(Some(0.5)), "0.50x"); + assert_eq!(format_scaling(Some(12.345)), "12.35x"); + assert_eq!(format_scaling(None), "--"); + assert_eq!(format_scaling(Some(f64::INFINITY)), "--"); + assert_eq!(format_scaling(Some(f64::NEG_INFINITY)), "--"); + assert_eq!(format_scaling(Some(f64::NAN)), "--"); +} + +#[test] +fn format_ratio_divides_and_guards_its_denominator() { + let fast = run(shapes::RESERVING_MPSC, 4, 200_000_000.0); + let slow = run(shapes::SLOTWISE_MPSC, 4, 100_000_000.0); + // slow is 10.0 ns/push, fast is 5.0, so fast/slow is 0.50x. + assert_eq!(format_ratio(Some(fast), Some(slow)), "0.50x"); + assert_eq!(format_ratio(Some(slow), Some(fast)), "2.00x"); + assert_eq!(format_ratio(None, Some(slow)), "--"); + assert_eq!(format_ratio(Some(fast), None), "--"); + assert_eq!(format_ratio(None, None), "--"); +} + +/// A shape that failed to run reports zero nanoseconds, and dividing by it would +/// put `inf` in a column a reader takes for a measurement. +#[test] +fn format_ratio_refuses_a_zero_denominator() { + let measured = run(shapes::RESERVING_MPSC, 4, 100_000_000.0); + let absent = run(shapes::SLOTWISE_MPSC, 4, 0.0); + assert_eq!( + absent.nanos_per_push, 0.0, + "the fixture must have zero cost" + ); + assert_eq!(format_ratio(Some(measured), Some(absent)), "--"); +} + +#[test] +fn format_nanos_renders_one_decimal_or_the_marker() { + assert_eq!( + format_nanos(Some(run(shapes::RESERVING_MPSC, 1, 1e9))), + "1.0" + ); + assert_eq!(format_nanos(None), "--"); +} + +#[test] +fn render_table_writes_a_header_and_one_line_per_run() { + let rows = vec![ + run(shapes::BASELINE_FETCH_ADD, 1, 400_000_000.0), + run(shapes::RESERVING_MPSC, 4, 100_000_000.0), + ]; + let mut out = String::new(); + render_table(&mut out, &rows); + let lines: Vec<&str> = out.lines().collect(); + assert_eq!(lines.len(), 3, "a header and two rows, got {out:?}"); + assert!(lines[0].contains("shape") && lines[0].contains("ns/push")); + assert!(lines[1].contains(shapes::BASELINE_FETCH_ADD)); + assert!(lines[2].contains(shapes::RESERVING_MPSC)); + assert!( + lines[2].contains("10.0"), + "100M pushes/sec is 10.0 ns/push, got {:?}", + lines[2] + ); +} + +#[test] +fn render_table_of_nothing_still_writes_its_header() { + let mut out = String::new(); + render_table(&mut out, &[]); + assert_eq!(out.lines().count(), 1, "header only, got {out:?}"); +} diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index f6c43d0a2..b2020001c 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -51,7 +51,7 @@ preferred. | D-14 | **`slotwise_mpsc`'s arming asks "would `pop` find something", not "is `len` zero".** The two disagree over a slot a producer has claimed but not published, and only the first answer lets the consumer park on it instead of spinning until that producer is rescheduled. | | D-15 | **`Doorbell::clear` resets the event *before* clearing the flag that mirrors it, and the original order was a lost wakeup.** A producer signalling between the two lines set the flag and issued a real `SetEvent`; the `ResetEvent` that followed erased the signal and left the flag set, wedging the doorbell dark while it claimed to be lit. **Amends [D-9](#d-9)**, whose "there is no third case" holds only for a queue whose emptiness is one position comparison. | | D-16 | **Its cost premise is falsified by [D-26](#d-26); the conclusion stands on capability instead -- see [D-29](#d-29).** Reservation is a capability a shape may lack, so the reserving multi-producer queue ships as a peer of `slotwise_mpsc` rather than replacing it. Honouring a reservation requires counting free slots, which requires the consumer's position -- a single shared line `slotwise_mpsc`'s push deliberately never reads. The original rationale added that this made reserving the *more expensive* shape and that both should ship rather than charge every caller for it; measurement reversed that, and the split is now justified by the capability alone. **Amends [D-6](#d-6)**, which assumed one queue would carry every policy. | -| D-17 | **The reservation count and the claim position live in one word, because a check-and-claim over both must be a single atomic operation.** Two atomics cannot be made correct with any amount of fencing: the pushing producer is load-then-store and the reserving one store-then-load, so the Dekker argument does not apply and both can miss each other. The 32/32 split is forced by the arithmetic, and caps this shape at 2^31 items. | +| D-17 | **Partly superseded by [D-41](#d-41): the 32/32 split is no longer forced, and the capacity ceiling is `Balanced`'s rather than the shape's.** **The reservation count and the claim position live in one word, because a check-and-claim over both must be a single atomic operation.** Two atomics cannot be made correct with any amount of fencing: the pushing producer is load-then-store and the reserving one store-then-load, so the Dekker argument does not apply and both can miss each other. The 32/32 split is forced *given the premise D-41 dropped* -- that the count must be able to reach the whole capacity -- and under that split the shape caps at 2^31 items on a 64-bit target, 2^30 on a 32-bit one. | | D-18 | **Superseded by [D-37](#d-37), which adopts a 128-bit compare-and-swap for a separate wide shape.** Retained because its analysis of the *costs* is still correct and D-37 depends on it; what changed is that those costs are now paid by a **separate shape** rather than imposed on this one. **Originally: a 128-bit compare-and-swap is refused.** **Amended once before being superseded, because three of the four reasons originally given were wrong or incomplete, and the decisive one was missing.** It would *not* lift the cap "and nothing else": a 64-bit position also collapses SH-14.1's ABA recurrence, which was unknown when this was written. It is *not* outside the x86-64 baseline -- `rustc 1.98.0` emits `target_feature="cmpxchg16b"` for `x86_64-pc-windows-msvc`, so there is no floor to raise and no runtime detection to pay. What stands is the dependency (`AtomicU128` is still unstable, rust-lang/rust#99069) and, decisively, that **`i686-pc-windows-msvc` has no 128-bit atomic at all**: adopting this is not "widen the word" but "widen the word *and* drop 32-bit support". Revisit for a tagged pointer, or if 32-bit support is dropped for other reasons -- not before. | | D-19 | **The coalesced loss latch is deliberately not generalised from the file watcher.** Coalescing there is sound because a desync is *idempotent* -- two mean the same as one, and the answer to both is a re-scan. A queue of arbitrary `T` has no such property, so what generalises is a loss *count*, which is M31.4's observability rather than a policy. | | D-20 | **Undrained items are handed to a caller-supplied sink at teardown, and the sink is chosen at construction because `Drop` has nowhere to hand them back to.** Without one they are destroyed on whichever thread released the last handle -- which may be a pool callback that must not block, and closing a handle to a dead network path can block for a long time. The default is unchanged; what changes is that it is now a named choice. | @@ -546,7 +546,8 @@ is unchanged and still describes `Balanced`, but its premise -- that the count m whole capacity -- was the thing D-41 dropped. Capping outstanding reservations instead frees the position to take 48 or 56 bits, so the split became a caller-selected layout rather than the only division of the word. Read "forced" below as "forced *given that premise*", and the 2^31 ceiling as `Balanced`'s rather -than the shape's. +than the shape's -- and on a 32-bit target as 2^30, where the crate-wide `usize` capacity bound binds +first. **The obvious implementation is broken, and it is worth writing down why, because the brokenness is not visible from reading either side on its own.** With the count in its own atomic: diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index ffaf33293..a1e0d0280 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -418,6 +418,11 @@ this host's 8 physical cores, and the spread across the three runs is not small: probe's own same-code control has been measured at 0.68-1.27x over seven runs, which is wide enough to swallow small differences; see [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). +That seven-run sweep is a **separate capture** taken to size the noise floor, not +a longer version of this table -- its medians differ from the ones above, which is +the point it was making. Where the two disagree, this table is the attributed +figure for this crate and the sweep is the evidence about how much such a figure +moves. **A previous version of this table compared two hosts** -- an AMD EPYC 7763 slice and a Snapdragon X2 Elite -- and has been removed rather than carried forward. Its From c83d411df8554a90dd9cbdcbd233b8615b976ac7 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 16:40:19 -0400 Subject: [PATCH 032/139] docs: require review feedback to be answered where it was raised Fixing the code and pushing is half of a review round; the reviewer being told what happened is the other half, and it is the half that gets dropped because the fix feels like completion. Three consequences, all observed on this repository: - a commit is not an answer, so nothing connects a change to the finding that asked for it, and the next round re-raises what was already addressed; - a correctly declined finding is indistinguishable from a missed one when the response is silence -- PR 90 carried three unresolved inline threads for several rounds whose finding was legitimately declined, but nothing said so; - suppressed comments have no thread at all, so there is no place a reply lands by default and no record that they were read. The new section says where each kind of feedback is answered (thread reply for inline, one PR comment per round for suppressed, a commit comment when there is no PR), and what the answer must contain: every finding either changed-with-SHA or declined-with-argument, sweeps reported as sweeps with their counts, and no claimed fix that was not verified by execution. Two further rules from this round: - The PR description restates measured claims and rationale, so it rots like any other prose -- but it is not a file in the tree, so no sweep, grep or CI check can reach it. Correcting it is now part of answering a round. PR 90's description was found carrying a client prescription that had already been swept out of the crate docs, and a "five rounds" history at round eighteen. - Incidental tallies stay out of the PR body. Test counts are the standing example: they change on almost every commit, say nothing that "the gate is green" does not, and manufacture a restatement-drift instance out of nothing. PR 90's body claimed "304 lib tests", which matched neither affected crate -- and correcting it by arithmetic produced a second wrong number before the figure was removed instead. Placed next to CONTRACT INTEGRITY, since a review response is where a sweep gets reported. Verified by executing the documented commands rather than by writing them down: `gh pr comment --body-file` and `gh api repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies` were both used to answer PR 90's outstanding rounds while drafting this. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 70 ++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d1e8f0905..c39e440f0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1271,9 +1271,75 @@ Two corollaries that have each already cost a review round: written while the old reading was current — generators, test doubles, examples — because those encode the reading rather than citing it. -## CHECKLIST file hygiene +## REVIEW FEEDBACK — answer it where it was raised, not only in the commit + +**A review round is not finished when the code changes. It is finished when the reviewer has +been told what happened.** Fixing the code and pushing is half the transaction; the other half +is a reply on GitHub, and omitting it is the default failure mode because the fix *feels* like +completion. It is not, for three reasons: + +- **A commit is not an answer.** The reviewer sees a new SHA, not your reasoning. Nothing + connects "I changed `format_ratio`" to the finding that asked for it, so the next round + re-raises what was already addressed — which has repeatedly cost rounds on this repository. +- **Some findings are correctly declined, and silence cannot say so.** A declined finding that + is never answered is indistinguishable from one that was missed. Declining is legitimate; + declining *silently* is not. +- **Suppressed comments have no thread at all.** They arrive in the review summary rather than + attached to a line, so there is no place a reply can land by default and no automatic record + that they were read. They are the easiest feedback to drop and the most likely to be re-raised + verbatim in the next round. + +### What to do, by where the feedback lives + +- **Inline review comments (a thread on a line).** Reply *on that thread*, naming what changed + and the commit SHA that changed it. Then resolve the thread — but only if the finding is + genuinely discharged; never resolve to clear the queue. Use the `resolveReviewThread` tool, or + `gh api repos/{owner}/{repo}/pulls/{number}/comments/{comment_id}/replies -f body=...`. +- **Suppressed comments, review-summary findings, and anything pasted to you out of band** — no + thread exists, so post **one new PR comment** covering that round: + `gh pr comment --body-file .scratch/.md`. One comment per round, not one per + finding; a reviewer reads the round as a unit. +- **No PR** (work committed straight to a branch, or feedback on a commit): + `gh api repos/{owner}/{repo}/commits/{sha}/comments -f body=...` against the commit that + carries the response. + +### What the response must contain + +Every finding in the round gets a line, and each line is one of exactly two things: + +1. **Changed** — what was changed and the SHA. Where the fix was a *sweep* rather than a + single-line edit (per CONTRACT INTEGRITY rule 3 above), say so and give the count: "swept + `QueueFull`: 13 files, 4 updated". A reviewer who sees only the cited line fixed has no way + to know the population was covered. +2. **Declined** — the argument for why, in enough detail to be argued back against. "Not + applicable" is not an argument; "this is gated behind `test-util`, so the mutant sits in code + the shipping build never compiles" is. + +Two further rules, each of which has already cost a round here: + +- **Do not claim a fix you have not verified.** The same standard applies as anywhere else in + this file: verify by execution. Where the fix was a test, say what sabotage showed it is + load-bearing — an unverified "added a test" is exactly the cosmetic binding CONTRACT INTEGRITY + rule 1 warns about. +- **Report what the round taught, not just what it touched.** When a review round reveals that + several findings were one underlying error, say that — it is more useful to the reviewer than + five separate acknowledgements, and it is how a recurring defect gets named instead of + repeatedly re-fixed. + +### The PR description drifts too, and nothing greps it + +A PR body is prose that restates measured claims, gate results, and design rationale — so it +rots exactly like the documents CONTRACT INTEGRITY governs, with one difference: **it is not a +file in the tree, so no sweep, grep, or CI check will ever catch it.** When a round corrects a +claim, check whether the PR description states the same claim, and correct it in the same round. + +Keep out of the PR body anything that drifts without carrying information. **Test counts are the +standing example**: "308 lib tests" changes on almost every commit, tells a reader nothing that +"tests pass" does not, and creates a restatement-drift instance out of nothing. State that the +gate is green and which parts of it ran; do not enumerate. The same goes for file counts, line +counts, and any other incidental tally that is not itself the finding. + -CHECKLIST files are **action-only**: they contain pending, in-progress, and recently completed (`[x]`) items awaiting migration to `COMPLETED-CHECKLIST.md`. Completed items must be moved to `COMPLETED-CHECKLIST.md` when a group is fully done (see below), with one exception: a **large** completed item is moved *immediately* and replaced in place by a From 23f6e0f35660c33fba4dc8c41dd3579c62fd0eaf Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 17:18:18 -0400 Subject: [PATCH 033/139] test(platform-probes): cover median_run's selection and the refusal column Three findings from the latest round. The first is a regression I introduced and shipped in c83d411, which is the one worth reading about. Inserting the REVIEW FEEDBACK section into copilot-instructions.md anchored the edit on the *following* section's heading and then failed to re-emit it, so `## CHECKLIST file hygiene` and its opening sentence were deleted. The checklist rules then sat under `### The PR description drifts too` until the next heading, mis-scoped as a subsection of PR-description advice. Restored, and verified by diffing the file against its pre-edit state: the section is now purely additive, with no deletions at all. Worth naming the mechanism, since the edit looked clean and the tests it broke do not exist -- anchoring an insertion on a neighbour's heading requires re-emitting that heading, and nothing checks it. `median_run` is the only code that turns five repetitions into a reported Run, and it took the timer as a closure, so it was always testable and simply had no test. Six now cover it, driven by a scripted timer: the median is selected rather than the first or last, the refusal count travels with the repetition whose duration was chosen, the warmup pass never reaches the report, both published rates derive from the one chosen repetition, and the median is divided by every producer's pushes. The fixture is built to catch two specific regressions, and does: sort by the wrong tuple element -> selects (5e6, 30), fails 3 tests sort removed entirely -> selects (5e6, 30), fails 3 tests It deliberately does NOT pin sort direction, and the finding's suggestion that it should is mistaken: REPETITIONS is 5, so results[REPETITIONS / 2] is index 2 of five, which is the median whether the sort ascends or descends. Verified rather than argued -- reversing the comparator leaves all 32 tests passing, because a reversed sort here is not a defect. The doc comment on the fixture records this so the gap reads as measured rather than missed. The renderer's refusal column had the same shape of gap: every existing test built rows refusing nothing, so a row that dropped the value would still render a plausible-looking table. It is the probe's only signal that a drained row was limited by consumer backpressure rather than claim contention. Covered with a nonzero fixture, and verified by hardcoding the column to 0 -- the two zero-refusal tests pass, the new one fails, which is exactly the finding's point. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 2 + .../src/queue_contention/tests.rs | 134 ++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c39e440f0..4e07744d1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1339,7 +1339,9 @@ standing example**: "308 lib tests" changes on almost every commit, tells a read gate is green and which parts of it ran; do not enumerate. The same goes for file counts, line counts, and any other incidental tally that is not itself the finding. +## CHECKLIST file hygiene +CHECKLIST files are **action-only**: they contain pending, in-progress, and recently completed (`[x]`) items awaiting migration to `COMPLETED-CHECKLIST.md`. Completed items must be moved to `COMPLETED-CHECKLIST.md` when a group is fully done (see below), with one exception: a **large** completed item is moved *immediately* and replaced in place by a diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index b22f4dabf..5977e144c 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -428,3 +428,137 @@ fn render_table_of_nothing_still_writes_its_header() { render_table(&mut out, &[]); assert_eq!(out.lines().count(), 1, "header only, got {out:?}"); } + +/// A timer that replays a scripted sequence instead of measuring anything, so +/// `median_run`'s selection can be checked exactly. The first value is consumed +/// by the untimed warmup pass. +fn scripted(values: Vec) -> impl FnMut(usize) -> Repetition { + let mut next = 0usize; + move |_producers| { + let value = values[next]; + next += 1; + value + } +} + +/// The scripted repetitions used by the tests below, in the order `median_run` +/// calls for them. Three properties are deliberate and each catches a different +/// regression: +/// +/// - the durations are **not** in ascending order, so failing to sort at all +/// selects 5e6 rather than the median 3e6; +/// - the refusal counts are **not** monotonic in duration, so sorting by the +/// wrong tuple element also selects 5e6; +/// - no two durations are equal, so the median is unambiguous. +/// +/// Sort *direction* is deliberately not covered, because it cannot be: with +/// `REPETITIONS == 5`, `results[REPETITIONS / 2]` is index 2 of five, which is +/// the median whether the sort ascends or descends. A test claiming to pin +/// direction here would pass under both and be theatre. +fn scripted_repetitions() -> Vec { + vec![ + (999e6, 9_999), // warmup, discarded + (4e6, 40), + (1e6, 10), + (5e6, 30), + (2e6, 50), + (3e6, 20), + ] +} + +#[test] +fn median_run_reports_the_median_repetition_rather_than_the_first_or_last() { + let measured = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); + // 3e6 ns over 1 * 50,000 pushes is 60 ns per push. + assert!( + (measured.nanos_per_push - 60.0).abs() < 1e-9, + "expected the 3e6 ns median, got {} ns/push", + measured.nanos_per_push + ); + assert_eq!(measured.shape, shapes::RESERVING_MPSC); + assert_eq!(measured.producers, 1); +} + +/// The refusal count travels with the repetition whose duration was chosen. It +/// is the probe's only signal that a drained row was limited by the consumer +/// rather than by claim contention, so pairing it with a different repetition +/// would misattribute the cause while leaving the timing plausible. +#[test] +fn median_run_pairs_the_refusal_count_with_the_median_repetition() { + let measured = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); + assert_eq!( + measured.refusals, 20, + "3e6 ns is the median and its repetition refused 20; got {}", + measured.refusals + ); +} + +/// The warmup pass exists to take the first-call costs out of the sample, so its +/// value must not reach the report. Its scripted duration is the largest in the +/// sequence and its refusal count is unique, so either leaking into the result +/// is visible. +#[test] +fn median_run_discards_the_warmup_pass() { + let measured = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); + assert_ne!( + measured.refusals, 9_999, + "the warmup's refusals were reported" + ); + assert!( + measured.nanos_per_push < 100.0, + "the warmup's 999e6 ns reached the report as {} ns/push", + measured.nanos_per_push + ); +} + +/// Both published rates come from the same chosen repetition, so they must agree +/// with each other. A regression that derived one from the median and the other +/// from some different element would leave a report whose two columns describe +/// different runs. +#[test] +fn median_run_derives_both_rates_from_the_one_chosen_repetition() { + let measured = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); + let round_trip = 1_000_000_000.0 / measured.nanos_per_push; + assert!( + (measured.pushes_per_second - round_trip).abs() < 1e-3, + "{} pushes/sec does not agree with {} ns/push", + measured.pushes_per_second, + measured.nanos_per_push + ); +} + +/// `median_run` scales by the producer count, so the same per-repetition +/// durations must report a lower per-push cost when more producers shared them. +#[test] +fn median_run_divides_the_median_by_every_producers_pushes() { + let one = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); + let four = median_run(shapes::RESERVING_MPSC, 4, scripted(scripted_repetitions())); + assert!( + (one.nanos_per_push / four.nanos_per_push - 4.0).abs() < 1e-9, + "four producers push four times as many items in the same span: {} vs {}", + one.nanos_per_push, + four.nanos_per_push + ); +} + +/// The refusal column is the probe's diagnosis of *why* a drained row is slow -- +/// consumer backpressure rather than claim contention -- so a row that dropped +/// or misformatted it would leave the report looking complete while the central +/// signal was silently absent. Every other renderer test builds rows refusing +/// nothing, which cannot catch that. +#[test] +fn render_table_shows_a_nonzero_refusal_count() { + let mut refused = run(shapes::SLOTWISE_MPSC, 8, 100_000_000.0); + refused.refusals = 123_456; + let mut out = String::new(); + render_table(&mut out, &[refused]); + let row = out.lines().nth(1).expect("one row was rendered"); + assert!( + row.contains("123456"), + "the refusal count is missing from {row:?}" + ); + assert!( + out.lines().next().expect("a header").contains("refusals"), + "the refusal column is unlabelled" + ); +} From e0bb76c4cb7dd53d09f2a7b06d5b9a229b79bea1 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 17:32:09 -0400 Subject: [PATCH 034/139] docs(waitable-queues): withdraw the producer-count reservation bound as false Five findings, of which three were one claim restated and that claim turned out to be wrong rather than merely worded badly. The crate said reservations were "bounded by how many producers are mid-send -- hundreds at most", and used that to argue a narrow reservation field gives up a ceiling nobody reaches. It is false: `Producer::reserve` takes `&self` and returns an owned `Reservation`, so one producer can hold as many as the field allows. Verified rather than reasoned about -- a new test reserves in a loop on a single handle and fills `Perpetual`'s 255 exactly, then is refused. The existing suite already held three reservations from one handle, so the evidence was in the tree the whole time. The correction matters because the false premise was load-bearing: it made 255 and 65,535 look unreachable, which was the argument for spending the bits on the position. The real bound is the lesser of the ring capacity and the layout's count field. The new test pins both halves -- shrinking its capacity below 255 makes capacity bind instead, which is how the test is known to measure the field rather than the ring. Swept four sites, not the three reported: lib.rs, README.md, DESIGN-NOTES.md's D-41, and a fourth in reserving_mpsc.rs's `ClaimLayout` rustdoc that the review did not name -- the paragraph a caller reads while choosing a layout, so the worst place for it. D-41 and the rustdoc record the withdrawal explicitly rather than quietly restating. Also: - The README's shape table had a `Choose it when` column carrying "**the default** for many producers" and "never in production". D-29 says this crate "publishes what it measured and declines to choose for the caller", so naming a winner between the two MPSC shapes contradicted it directly. The column is now `Applies when` and every cell states a capability the caller matches against. An earlier sweep removed layout prescriptions and missed this table because it was looking for layout wording. - The `Wide` portability paragraph said an unconditional wide word would be "silently mutex-backed" on a target without a native 128-bit exchange. That describes `portable-atomic`'s defaults, not this crate: Cargo.toml takes the dependency with `default-features = false` precisely so that substitution cannot happen, and says so. On such a target `AtomicU128` does not exist and enabling `dwcas` fails the build. The paragraph now distinguishes the hazard avoided from the behaviour shipped. Gate: fmt, clippy --all-targets, lib/doc tests under --all-features including the compiled README, and rustdoc with -D warnings for the new intra-doc links. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 14 +++++---- crates/windows-waitable-queues/src/lib.rs | 9 ++++-- .../src/reserving_mpsc.rs | 24 +++++++++++---- .../src/reserving_mpsc/tests.rs | 30 +++++++++++++++++++ 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index b2020001c..33041f5a6 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: a 32-bit field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, where the real bound is however many producers are mid-send. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: a 32-bit field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, where the bound that matters is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index a1e0d0280..914d19f44 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -64,12 +64,12 @@ how many threads push, whether a slot can be claimed before the message exists -- decides its *algorithm*, not merely its configuration, so these are separate shapes rather than one type with switches. A caller names the shape it wants. -| Shape | Producers | What it adds | Choose it when | +| Shape | Producers | What it adds | Applies when | |---|---|---|---| | `spsc` | one | nothing -- no compare-and-swap on either side | exactly one thread pushes | -| `slotwise_mpsc` | many | Vyukov's per-slot sequence protocol, so producers push without a lock | **the default** for many producers | +| `slotwise_mpsc` | many | Vyukov's per-slot sequence protocol, so producers push without a lock | many threads push and a full queue may refuse | | `reserving_mpsc` | many | claiming a slot *before* the message exists | a message must not be lost to a full queue | -| `permit_mpsc` | many | an experimental claim protocol | never in production -- see below | +| `permit_mpsc` | many | an experimental claim protocol | behind `experimental-permit-claim`, outside the semver promise -- see below | Every shape has one consumer. `permit_mpsc` is behind the non-default `experimental-permit-claim` feature and is outside the semver promise; it will @@ -119,9 +119,11 @@ to correct that misreading once. **This is a property of the default layout, not of the shape**, and that is a change: it was previously a defect a caller had to live with. The claim word packs an outstanding-reservation count beside the position, and how its bits are -divided is now a caller's choice. Reservations are bounded by how many producers -are mid-send -- hundreds at most -- so giving up a ceiling nobody reaches buys -positions: +divided is now a caller's choice. A narrower count field buys position bits, and +what it costs is reservations held simultaneously: `Producer::reserve` takes +`&self` and returns an owned `Reservation`, so a single producer can hold as +many as the field allows, and a caller that holds many at once is choosing +against the narrower layouts rather than against a producer count. | Layout | Reservation-count field ceiling | Pushes to recurrence | At sustained maximum rate | |---|---|---|---| diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 54e5d4281..d9f7abcec 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -87,9 +87,12 @@ //! **This is a property of the default layout, not of the shape**, and that is //! a change: it was previously a defect a caller had to live with. The claim //! word packs an outstanding-reservation count beside the position, and how its -//! bits are divided is now a caller's choice. Reservations are bounded by how -//! many producers are mid-send -- hundreds at most -- so giving up a ceiling -//! nobody reaches buys positions: +//! bits are divided is now a caller's choice. A narrower count field buys +//! position bits, and what it costs is reservations held simultaneously: +//! `Producer::reserve` takes `&self` and returns an owned `Reservation`, so a +//! single producer can hold as many as the field allows, and a caller that +//! holds many at once is choosing against the narrower layouts rather than +//! against a producer count. //! //! | Layout | Reservation-count field ceiling | Pushes to recurrence | At sustained maximum rate | //! |---|---|---|---| diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 7a73f7f7f..f318d88f5 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -140,9 +140,15 @@ //! The reason for keeping it out of the default is unchanged: widening this //! shape's word unconditionally would change what the module offers depending on //! the target, because `i686-pc-windows-msvc` has no lock-free 128-bit exchange -//! and neither does an x86-64 build without `cmpxchg16b`. The same module would -//! be lock-free on one target and silently mutex-backed on another. So `Wide` -//! (which exists only under `dwcas`, so this names it without linking) is +//! and neither does an x86-64 build without `cmpxchg16b`. On such a target this +//! crate does not silently substitute a lock: the `portable-atomic` dependency +//! is taken with `default-features = false`, which is load-bearing precisely +//! because its defaults *would* supply a global lock and still compile. With +//! them off, `AtomicU128` does not exist there and enabling `dwcas` fails the +//! build naming it. So the hazard an unconditional wide word would carry is a +//! module that is lock-free on one target and mutex-backed on another; the +//! hazard the feature gate actually trades it for is a build that stops. So +//! `Wide` (which exists only under `dwcas`, so this names it without linking) is //! reached by naming it, and the //! narrow word's contract is identical on every target -- a caller gets the wide //! one by asking, never by accident of where they compiled. @@ -178,8 +184,16 @@ use crate::options::Options; /// /// **The two things being traded are not equally valuable, and the shipping /// default spends the bits on the less valuable one.** The reservation count -/// bounds how many messages may be held in flight at once -- in practice the -/// number of producers mid-send, so hundreds or thousands. The position decides +/// bounds how many messages may be held in flight at once. That bound is the +/// lesser of the ring's capacity and the layout's count field, and it is +/// reachable by a *single* producer: [`Producer::reserve`] takes `&self` and +/// returns an owned [`Reservation`], so one thread can hold as many as the +/// field allows. (An earlier version of this paragraph said the practical bound +/// was "the number of producers mid-send, so hundreds or thousands". That was +/// wrong, and it mattered -- it made the narrower fields look unreachable. One +/// producer alone fills `Perpetual`'s 255 and is then refused, which +/// `one_producer_alone_can_exhaust_the_reservation_field` pins.) The position +/// decides /// how many pushes occur before it recurs, and a recurrence is the `SH-14.1` /// hazard: a producer descheduled across a full wrap can claim against a /// numerically identical but generations-later value. diff --git a/crates/windows-waitable-queues/src/reserving_mpsc/tests.rs b/crates/windows-waitable-queues/src/reserving_mpsc/tests.rs index 7223392d3..667d56754 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc/tests.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc/tests.rs @@ -1765,3 +1765,33 @@ fn try_iter_and_drain_are_the_same_iterator_and_need_no_import() { let taken: Vec = rx.drain().collect(); assert_eq!(taken, vec![4, 5]); } + +/// A single producer handle can hold many reservations at once, because +/// `reserve` takes `&self` and returns an owned `Reservation`. The +/// reservation-count field is therefore reachable by ONE producer in a loop, +/// and the bound has nothing to do with how many producers exist. +/// +/// This is asserted because the crate's documentation once claimed the opposite +/// -- that reservations were "bounded by how many producers are mid-send" -- +/// which would have made `Perpetual`'s 255 ceiling unreachable in practice. It +/// is reachable by one thread, and this pins that. +#[test] +fn one_producer_alone_can_exhaust_the_reservation_field() { + // Perpetual is 8/56: the count field holds at most 255. + let (tx, _rx) = bounded_as::(1024).expect("a valid capacity"); + + let held: Vec<_> = std::iter::repeat_with(|| tx.reserve()) + .take_while(Option::is_some) + .flatten() + .collect(); + + assert_eq!( + held.len(), + Perpetual::MAX_RESERVED as usize, + "one producer filled the field to its ceiling, not to a producer count" + ); + assert!( + tx.reserve().is_none(), + "the field is full, so the next reservation must be refused" + ); +} From b1731985754f0d5a3c171a04b38cc0d333c304d8 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 17:50:59 -0400 Subject: [PATCH 035/139] docs: record that restatement count, not prose volume, is the error surface Written to inform a question that is not yet decided: whether this repository says too much, and whether some formal specification plus substantially less prose would shrink the error surface. Measured rather than argued. Prose runs at 0.84 lines per line of code across the workspace -- about 86,000 against 101,700. That number turns out to be the wrong one to watch. In windows-waitable-queues, single facts are restated 3 to 19 times across 3 to 5 files: "255" nineteen times, "37 seconds" eight, "2^56" six. Every one is derivable from ClaimLayout's associated constants and every one is hand-maintained with nothing checking it. Halving the prose uniformly would leave half of each row and fix nothing structural. Sorting PR 90's findings by class predicts which remedy helps. Restated derivable facts are the large majority; structural defects want a linter; evidence overclaiming wants MORE measurement, not less prose; policy needs a reviewer. Algorithm properties -- where TLA+ and loom live -- produced zero findings in any round, and the honest reading of that is not that the algorithms are good but that there is no instrument. SH-14.1 is a live known defect found by a person reasoning carefully, and nothing in the toolchain would have caught it. Absence of findings where nothing looks is not evidence of correctness, which is an error this repository has already corrected in its own measurements. Three conclusions, of which the middle changes practice: formal specification and prose reduction address different classes and should not be conflated; the cut must be to restated assertions rather than to rationale, because no finding in any round was against a passage explaining why a decision was made; and a formal spec's most useful property here is not proof but that prose can point at it instead of paraphrasing it -- restatement drift's first remedy, one level up. Records the cheapest available move without scheduling it: README.md is already a build input, so a test could assert every published layout row against ClaimLayout's constants, which would mechanically have caught the 2^31/2^30 target error, the MAX_RESERVED-as-capacity conflation and both stale recurrence tables. No checklist item is created. The absence is deliberate per "design notes are not a work queue" and is stated in the note so it does not read as an oversight. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 2ce928d50..a13ee6fc3 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1859,3 +1859,98 @@ written. The rule is about **discarded failure information**, not about discarde The audit this decision implies is queued as [CHECKLIST.md](CHECKLIST.md) -> `M22.1`; it is not scheduled by this note alone. + +## Prose volume is not the error surface; restatement count is + +[Restatement drift](#restatement-drift) explains the mechanism and gives the remedy. This note +records something that section does not: a measurement of **where** the drift actually lives, taken +after PR #90's eighteenth review round, and what follows from it about formal specification. + +The question that prompted it was whether this repository simply says too much -- whether English, +which must be inexact to serve human readers, is being asked to carry a specification load it cannot +bear, and whether some formal specification plus substantially less prose would shrink the error +surface. + +### The measurement + +Across the workspace, prose runs at **0.84 lines per line of code** -- about 86,000 lines of prose +(50,700 Rust comment lines, 35,200 markdown) against 101,700 lines of code. + +That number turns out to be the wrong one to watch. In `windows-waitable-queues`, the crate that +produced most of the review findings, single facts are restated like this: + +| fact | restatements | files | +|---|---|---| +| `255` (the `Perpetual` reservation-count ceiling) | 19 | 5 | +| `37 seconds` (the `Balanced` recurrence horizon) | 8 | 4 | +| `2^56` (the `Perpetual` position span) | 6 | 3 | +| `4,294,967,295` (the `Balanced` field ceiling) | 5 | 3 | +| `about 20 years` | 3 | 3 | + +**Every one of these is derivable from `ClaimLayout`'s associated constants, and every one is +hand-maintained with nothing checking it.** The error surface is proportional to that column, not to +total prose volume. Halving the prose uniformly would leave roughly half of each row and fix +nothing structural. + +### Which errors this predicts, and which it does not + +Sorting PR #90's findings across all rounds by class: + +- **Restated derivable facts** -- the `2^31`/`2^30` target-dependent capacity, `MAX_RESERVED` + conflated with capacity, "`Wide` removes it" for a bound that is finite, stale recurrence tables, + a test count that matched no crate. **The large majority.** +- **Structural** -- an unmarked supersedence row in a decision index, an orphaned milestone + reference. A linter's job, not a specification's. +- **Evidence overclaiming** -- a noise floor computed from two runs, a refusal-count argument that + did not reproduce in direction or magnitude across three re-measurements. These were the most + valuable findings of the whole PR, and *more* measurement is what fixes them, not less prose. +- **Policy** -- client prescriptions surviving [D-no-client-prescriptions](crates/windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions). + Only a reviewer catches these. +- **Algorithm properties** -- **zero findings, in any round.** + +That last line is the one to be careful with, because it has two readings and only the second is +honest. There are no findings in that class because **there is no instrument for it**, not because +the algorithms are known good. `SH-14.1` is a live, known defect in the claim protocol; it was found +by a person reasoning carefully, and nothing in the toolchain would have caught it. Absence of +findings where nothing looks is not evidence of correctness -- the same error this repository has +corrected in its own measurements more than once. + +### What follows + +Three conclusions, of which the middle one is the one that changes practice. + +**Formal specification and prose reduction address different classes.** TLA+ and `loom` +([D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31)) target algorithm properties, which +have produced no findings and carry one known unfound defect. Restatement targets documented facts, +which have produced most findings. Both are worth doing; conflating them would aim the expensive +instrument at the cheap problem. + +**The cut must be to restated assertions, not to rationale.** No finding in any round of PR #90 was +against a passage explaining *why* a decision was made. The findings were against duplicated +*assertions* of fact, against overclaims from evidence, and against prescriptions. Rationale is what +makes a decision re-checkable years later and is the reason this file exists at all; cutting it +uniformly to hit a volume target would remove the only prose that has never been wrong, while +leaving the prose that keeps being wrong in proportion. + +**A formal spec's most useful property here is not proof -- it is that prose can point at it instead +of paraphrasing it.** That is [restatement drift](#restatement-drift)'s first remedy applied one +level up: define the protocol once in a form that can be checked, and let every document cite it. +This is the real connection between the two ideas, and it is why they belong in the same +conversation despite fixing different things. + +### The cheapest available move, recorded but not scheduled + +`README.md` is already a build input for `windows-waitable-queues` (`#[doc = include_str!]` in +[lib.rs](crates/windows-waitable-queues/src/lib.rs)), so a test can parse the published layout +tables and assert every row against `ClaimLayout`'s constants -- converting 19 hand-written `255`s +into one definition and N checked derivations, with no generator and no new tooling. Mechanically, +that would have caught the `2^31`/`2^30` error, the `MAX_RESERVED`-as-capacity conflation, and both +stale recurrence tables. + +**No work is scheduled by this note.** It was written to inform a decision that has not been taken, +and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule +rather than an oversight. The formal-methods survey it bears on is `M30` in the root +[CHECKLIST.md](CHECKLIST.md), which is not yet on `main` -- it is pending in PR #92, where the +milestone was originally numbered `M23` until that ID was found to collide with an archived one. If +the table-versus-constants test or a prose-reduction pass is adopted, each needs its own item at +that time. From 679dd8eb32a7724982d891b165b8af09419c51b9 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 19:07:05 -0400 Subject: [PATCH 036/139] docs: the wrap exposure is a count, not a rate, and 6.4x came from withdrawn data Seven findings, of which two were a logic error in a hazard condition and one was a stale figure the review did not report. The SH-14.1 disclosure said a queue "not driven at sustained maximum rate by two or more producers is not exposed". That is false and it is the worst sentence in the crate to have wrong, because a caller uses it to decide whether SH-14.1 applies to them. The position advances once per push regardless of arrival rate, so a slow queue with two or more producers reaches the same wrap, later. The exposure is a conjunction of two counts -- two or more producers, and 2^32 pushes over the life of one queue -- and rate determines only when. Corrected in the README and the crate rustdoc, which carried it identically. Sweeping the shape-comparison claim turned up something not reported: the "by up to 6.4x" figure in reserving_mpsc's and slotwise_mpsc's public rustdoc comes from the ARM64 half of a two-host capture that the README already records as withdrawn for predating the timing-window correction. Two public rustdocs were therefore publishing a magnitude whose source this branch had explicitly retired, and D-29 restated it a third time. The rustdocs now point at the attributed table instead of carrying a number, and D-29 keeps its figures labelled as the historical record of what reopened the question, since the direction survives the correction and the magnitudes do not. The same paragraph asserted that reserving_mpsc is faster *because* slotwise's slot sequence marches through memory while other producers write it. The probe times the complete push and cannot isolate or bound that read, so the mechanism is plausible and unestablished; the causal claim is withdrawn and labelled. Also removed the surviving "see the crate documentation ... for how to choose", which the prescriptions sweep missed in slotwise_mpsc. A sixth site of the withdrawn producer-count reservation bound, in the probe's own design notes, using "mid-flight" where the previous five said "mid-send" -- one word outside the regex that swept the other five. The instrument was rewritten to search the concept (a reservation quantity bounded by a producer quantity, any vocabulary) rather than the phrasings, which finds it and finds nothing else outstanding. D-37's "re-apportioning removes the exposure" is the same class as the earlier "Wide removes it" and now says it moves the exposure to 2^48 or 2^56. Two against the note added an hour ago. It claimed every restated figure was derivable from ClaimLayout's constants; the two time rows also need an assumed push rate, so that overstated what a constants-versus-table check could validate -- in a note about overstatement. Corrected, with the consequence for the proposed remedy stated rather than buried. The README reference is now a clickable relative link per the repository convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 19 +++++++++---- .../windows-platform-probes/DESIGN-NOTES.md | 27 +++++++++++++------ .../windows-waitable-queues/DESIGN-NOTES.md | 10 +++++-- crates/windows-waitable-queues/README.md | 10 ++++--- crates/windows-waitable-queues/src/lib.rs | 10 ++++--- .../src/reserving_mpsc.rs | 17 +++++++++--- .../src/slotwise_mpsc.rs | 8 ++++-- 7 files changed, 74 insertions(+), 27 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index a13ee6fc3..1f2c3599c 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1887,8 +1887,14 @@ produced most of the review findings, single facts are restated like this: | `4,294,967,295` (the `Balanced` field ceiling) | 5 | 3 | | `about 20 years` | 3 | 3 | -**Every one of these is derivable from `ClaimLayout`'s associated constants, and every one is -hand-maintained with nothing checking it.** The error surface is proportional to that column, not to +**All of these are restated by hand with nothing checking them, and most are derivable from +`ClaimLayout`'s associated constants.** The two time rows are not: `37 seconds` and `about 20 years` +follow from a field width *and* an assumed sustained push rate, so a constants-versus-table check +would validate the first three rows outright and the time rows only once the rate is also pinned +somewhere single. That distinction matters because it bounds what the cheapest remedy below can +actually do -- an earlier version of this paragraph said every row was derivable from the constants, +which overstated it, in a note about overstatement. The error surface is proportional to that column, +not to total prose volume. Halving the prose uniformly would leave roughly half of each row and fix nothing structural. @@ -1940,12 +1946,15 @@ conversation despite fixing different things. ### The cheapest available move, recorded but not scheduled -`README.md` is already a build input for `windows-waitable-queues` (`#[doc = include_str!]` in +[README.md](crates/windows-waitable-queues/README.md) is already a build input for +`windows-waitable-queues` (`#[doc = include_str!]` in [lib.rs](crates/windows-waitable-queues/src/lib.rs)), so a test can parse the published layout tables and assert every row against `ClaimLayout`'s constants -- converting 19 hand-written `255`s into one definition and N checked derivations, with no generator and no new tooling. Mechanically, -that would have caught the `2^31`/`2^30` error, the `MAX_RESERVED`-as-capacity conflation, and both -stale recurrence tables. +that would have caught the `2^31`/`2^30` error, the `MAX_RESERVED`-as-capacity conflation, and the +ceiling and push-count columns of both stale recurrence tables. The time columns need the assumed +rate pinned somewhere single before they can be checked the same way, which is a second and smaller +piece of work rather than a reason not to do the first. **No work is scheduled by this note.** It was written to inform a decision that has not been taken, and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 362114e62..17c46d956 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -910,14 +910,25 @@ conservative floor on time-to-wrap. 33M/s is the measured drained rate at one producer. 116M/s is the crate's own disclosed figure and is the honest planning number. -**The reservation half is where the bits are being spent, and it is the half -worth least.** Outstanding reservations are bounded by how many producers are -mid-flight -- hundreds, perhaps thousands -- and the field currently holds four -billion. Giving up reservations nobody will allocate is what buys the position -bits: 2^21 reservations leaves about a day, 2^12 leaves over a year, and 2^8 -leaves twenty years. The last reaches the same practical headroom a 128-bit word -gives, on a plain `AtomicU64`, without a third-party dependency and without -reopening `D-18`'s i686 question. +**The reservation half is where the bits are being spent, and the trade it makes +is a real one.** The field currently holds four billion outstanding reservations. +Narrowing it is what buys the position bits: 2^21 reservations leaves about a +day, 2^12 leaves over a year, and 2^8 leaves twenty years. The last reaches the +same practical headroom a 128-bit word gives, on a plain `AtomicU64`, without a +third-party dependency and without reopening `D-18`'s i686 question. + +**An earlier version of this paragraph called the reservation half "the half +worth least", on the premise that outstanding reservations are bounded by how +many producers are mid-flight -- hundreds, perhaps thousands -- so that narrowing +the field gave up "reservations nobody will allocate". That premise is +withdrawn as false.** `Producer::reserve` takes `&self` and returns an owned +`Reservation`, so one producer can hold as many as the field allows: the bound is +the lesser of the ring capacity and the field, not a producer count. The queue +crate's `one_producer_alone_can_exhaust_the_reservation_field` fills `Perpetual`'s +255 from a single thread and is then refused. The arithmetic above is unaffected, +but what it costs is not free -- a caller holding many simultaneous reservations +is choosing against the narrower layouts. See +[D-41](../windows-waitable-queues/DESIGN-NOTES.md#d-41). **This paragraph previously added "at no measured cost", and that clause is withdrawn** -- it was the same claim the layout section below withdrew, restated diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 33041f5a6..398a398d0 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -71,7 +71,7 @@ preferred. | D-34 | **Every bounded queue surveyed is ABA-safe for one of two reasons, and this crate's `reserving_mpsc` has neither.** Either the claim counter is a whole machine word, so recurrence is unreachable -- crossbeam, concurrent-queue, thingbuf, Vyukov, SCQ's `Head`/`Tail` -- or the authorizing compare-exchange is moved onto the cell, so the decision and the write are validated together (CRQ, SCQ). Ours packs the position into a 32-bit *subfield* and authorizes with an exchange that does not cover the separately-read `head`. Nikolaev (DISC 2019, section 3) states the width assumption the field relies on and states it for **CPU-word** width, which a subfield does not satisfy; DPDK's `rte_ring` is the same protocol as ours and its published justification covers modular arithmetic only. The generalisation -- ours, unstated in any source -- is that **the atomic operation authorizing the write must cover everything the decision depended on.** Survey in [DESIGN-SESSION-2026-09-02](design-sessions/DESIGN-SESSION-2026-09-02-claim-protocol-prior-art.md); the fix is the claim-protocol replacement recorded there. | | D-35 | **Measured: the permit claim is 2.7x faster than `reserving_mpsc` at 16-32 producers, and 1.45x slower at one.** The safer claim is also the faster one everywhere contention exists, which was not the expected result -- it touches *two* shared lines where the shipping shape touches one plus a read, and [D-26](#d-26) had established that the shared line is what collapses. The mechanism is that both of its operations are unconditional read-modify-writes that never retry, where the shipping shape's compare-exchange retries once per lost race; the retries dominate long before the second line does. It is the only shape measured that gets *faster* per push as producers are added (42.8 ns at two to 19.5 at thirty-two) and the only one that stays within 1.5x of a bare contended `fetch_add`. **This decides the shape of the fix but not the fix**: the drained regime's refusal counts differ by orders of magnitude in a way this harness cannot attribute, which SH-15.5.1 exists to settle before SH-15.6 adopts anything. | | D-36 | **Superseded by [D-41](#d-41): the hazard is now a layout choice, not a defect that must ship.** The reasoning below stands as the record of why it was right to disclose rather than delay while the only known fix was the claim-protocol replacement. **0.1.0 ships SH-14.1 disclosed rather than fixed, and the disclosure is a release blocker.** Following [D-31](#d-31)'s principle -- the disclosure, not the deferral, is the decision -- because the fix is a claim-protocol replacement ([D-35](#d-35)) whose adoption is still gated on an open question, and holding the release for it would trade a *documented* hazard for an undocumented rush. **The two gaps are not equally forgiving and the text says so**: an unverified ordering is a risk of a bug, this is a known one with a computed exposure, and its failure mode is silent -- no error, panic, or counter -- so a caller can neither detect nor mitigate it. That is precisely why it may not ship in silence. Stated in the crate docs, the README, and the shape's own module docs, each leading with **"on every target, not only 32-bit ones"**, because the natural spelling "32-bit position" invites the opposite reading and SH-6.1 already had to be corrected for exactly that. The shape-selection guidance in both documents was also amended: it previously said "start with `reserving_mpsc`" with no caveat, pointing callers at the hazardous shape by default. | -| D-37 | **Partly superseded by [D-41](#d-41): the wide word ships as a *layout* behind the non-default `dwcas` feature, not as a separate `reserving_mpsc_wide` shape, and the gate is the feature rather than the target.** What stands is the reasoning below about `portable-atomic`: `default-features = false` is load-bearing, because with defaults on it silently substitutes a global lock, and D-7's burden of proof is discharged rather than waived. What does not is the shape's name and the premise that the narrow word must keep SH-14.1 -- re-apportioning the narrow word removes the exposure without a third-party dependency -- what it costs in throughput is unestablished, see [D-41](#d-41) -- so the wide word is no longer the only way out. **The reserving claim word ships in two widths: the narrow one on every target, the wide one only where a 128-bit exchange is genuinely lock-free.** `reserving_mpsc` keeps its packed 64-bit word, keeps SH-14.1's hazard, keeps [D-36](#d-36)'s warnings, and is **never silently swapped** for the wide shape on targets that could host one -- a contract that changes with the target is what PLATFORM INTEGRITY rule 2 forbids, and a caller who read "2^32" must get 2^32. `reserving_mpsc_wide` is the same protocol with a `u128` word split 64/64: recurrence needs 2^64 pushes, and the capacity ceiling rises to 2^62. **The gate is one line of `Cargo.toml`: `default-features = false`.** Measured, not designed -- with the default feature set `portable-atomic` compiles on i686 and silently substitutes a global lock, but with defaults off `AtomicU128` **does not exist** there (`no AtomicU128 in the root`), nor on x86_64 built without `cmpxchg16b`. It exists exactly where a native lock-free exchange is guaranteed at compile time, so the `use` statement is the gate and it fails loudly. A `cfg(target_has_atomic = "128")` would be the *wrong* gate -- it is emitted even with `cmpxchg16b` disabled -- and a `const` assertion on `is_always_lock_free()`, though genuinely const-evaluable, is redundant where the type exists and unreachable where it does not. That is the standard SH-14.2 already set when it probed i686 to confirm `AtomicU64` was lock-free before widening `slotwise_mpsc`, recording that a hidden mutex "would have made this a bad trade". [D-7](#d-7)'s burden of proof for adding a Cargo feature is **discharged, not waived**: D-7 rejected feature-gating because the only benefit was compile time, and the cost here is a third-party dependency, which dead-code elimination does not remove from `Cargo.lock` or from an auditor's review. | +| D-37 | **Partly superseded by [D-41](#d-41): the wide word ships as a *layout* behind the non-default `dwcas` feature, not as a separate `reserving_mpsc_wide` shape, and the gate is the feature rather than the target.** What stands is the reasoning below about `portable-atomic`: `default-features = false` is load-bearing, because with defaults on it silently substitutes a global lock, and D-7's burden of proof is discharged rather than waived. What does not is the shape's name and the premise that the narrow word must keep SH-14.1 -- re-apportioning the narrow word moves the exposure to a later recurrence (`2^48` under `Enduring`, `2^56` under `Perpetual`) without a third-party dependency -- what it costs in throughput is unestablished, see [D-41](#d-41) -- so the wide word is no longer the only way out. **The reserving claim word ships in two widths: the narrow one on every target, the wide one only where a 128-bit exchange is genuinely lock-free.** `reserving_mpsc` keeps its packed 64-bit word, keeps SH-14.1's hazard, keeps [D-36](#d-36)'s warnings, and is **never silently swapped** for the wide shape on targets that could host one -- a contract that changes with the target is what PLATFORM INTEGRITY rule 2 forbids, and a caller who read "2^32" must get 2^32. `reserving_mpsc_wide` is the same protocol with a `u128` word split 64/64: recurrence needs 2^64 pushes, and the capacity ceiling rises to 2^62. **The gate is one line of `Cargo.toml`: `default-features = false`.** Measured, not designed -- with the default feature set `portable-atomic` compiles on i686 and silently substitutes a global lock, but with defaults off `AtomicU128` **does not exist** there (`no AtomicU128 in the root`), nor on x86_64 built without `cmpxchg16b`. It exists exactly where a native lock-free exchange is guaranteed at compile time, so the `use` statement is the gate and it fails loudly. A `cfg(target_has_atomic = "128")` would be the *wrong* gate -- it is emitted even with `cmpxchg16b` disabled -- and a `const` assertion on `is_always_lock_free()`, though genuinely const-evaluable, is redundant where the type exists and unreachable where it does not. That is the standard SH-14.2 already set when it probed i686 to confirm `AtomicU64` was lock-free before widening `slotwise_mpsc`, recording that a hidden mutex "would have made this a bad trade". [D-7](#d-7)'s burden of proof for adding a Cargo feature is **discharged, not waived**: D-7 rejected feature-gating because the only benefit was compile time, and the cost here is a third-party dependency, which dead-code elimination does not remove from `Cargo.lock` or from an auditor's review. | | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | @@ -1162,7 +1162,13 @@ information we have. A `0.x` version number carries the rest, and is meant liter [D-26](#d-26) falsified [D-16](#d-16)'s premise -- reading the consumer's position was supposed to make `reserving_mpsc` the expensive shape, and it is instead the faster one under contention, by up to 4x on -x64 and 6.4x on ARM64. That reopened a question D-16 had treated as settled: if the split does not buy +x64 and 6.4x on ARM64. **Those two figures predate the timing correction described in +[DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations) and are +retained only as the record of what reopened the question**; the two-host capture they came from has +been withdrawn from the README rather than carried forward, and no current public figure is derived +from them. The direction they established -- that `reserving_mpsc` is not the expensive shape -- +survives the correction; the magnitudes do not. That reopened a question D-16 had treated as settled: +if the split does not buy what it claimed, should the shapes merge, or should one be deleted? **Neither. Both ship, and the crate declines to choose between them on the caller's behalf.** diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 914d19f44..e882e4a1a 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -203,9 +203,13 @@ proportionally longer to reach its wrap. are 64 bits on every target, so the equivalent wrap needs 2^64 claims. It does not offer `Reserving`. - **`spsc` never had it**, having no contended claim to race. -- **The default layout is sound below its wrap.** A queue that will not push 4.3 - billion items in one run, or that is not driven at sustained maximum rate by - two or more producers, is not exposed even on `Balanced`. +- **The default layout's exposure is a count, not a rate.** Two conditions must + both hold: two or more producers (one producer has no race to lose), and 4.3 + billion pushes accumulated over the life of one queue. A lower sustained rate + does not remove the exposure -- the position advances once per push regardless + of how fast they arrive, so a slow queue with two or more producers reaches the + same wrap, just later. An earlier version of this bullet listed a low rate as + its own exemption, which was wrong. This is disclosed on the same principle as the ordering gap below: an adopter gets the information we have rather than an assurance we cannot support. The diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index d9f7abcec..19151d4ad 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -176,9 +176,13 @@ //! positions are 64 bits on every target, so the equivalent wrap needs 2^64 //! claims. It does not offer [`Reserving`]. //! - **[`spsc`] never had it**, having no contended claim to race. -//! - **The default layout is sound below its wrap.** A queue that will not push -//! 4.3 billion items in one run, or that is not driven at sustained maximum -//! rate by two or more producers, is not exposed even on `Balanced`. +//! - **The default layout's exposure is a count, not a rate.** Two conditions +//! must both hold: two or more producers (one producer has no race to lose), +//! and 4.3 billion pushes accumulated over the life of one queue. A lower +//! sustained rate does not remove the exposure -- the position advances once +//! per push regardless of how fast they arrive, so a slow queue with two or +//! more producers reaches the same wrap, just later. An earlier version of +//! this bullet listed a low rate as its own exemption, which was wrong. //! //! This is disclosed on the same principle as the ordering gap below: an //! adopter gets the information we have rather than an assurance we cannot diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index f318d88f5..4799e7a9d 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -59,10 +59,19 @@ //! is what `slotwise_mpsc` avoids and why it cannot offer reservation at all. //! //! **That cost is not what makes either shape slower.** This one measured -//! *faster* than `slotwise_mpsc` under contention on both architectures tried, by up to -//! 6.4x, because the slot sequence `slotwise_mpsc` reads instead marches through memory -//! while other producers write it. See the crate documentation for the numbers -//! and for how to choose. +//! *faster* than `slotwise_mpsc` under contention on the hosts tried. The +//! magnitude belongs with the capture that produced it rather than here, so see +//! the crate documentation's attributed table for the figures and the conditions +//! they were taken under. +//! +//! An earlier version of this paragraph gave a figure ("by up to 6.4x") taken +//! from a two-host comparison that has since been withdrawn for predating a +//! correction to the probe's timing window, and attributed the difference to +//! `slotwise_mpsc`'s slot sequence marching through memory while other producers +//! write it. That mechanism is plausible and is **not** established: the probe +//! times the complete push and cannot isolate or bound that read, so the causal +//! claim went further than the measurement supports. The direction survives; the +//! magnitude and the cause do not. //! //! `slotwise_mpsc`'s producer never reads the consumer's position. It asks a different //! question -- "is the slot I am about to claim free?" -- and reads that from diff --git a/crates/windows-waitable-queues/src/slotwise_mpsc.rs b/crates/windows-waitable-queues/src/slotwise_mpsc.rs index 89b8f2197..ddab6187e 100644 --- a/crates/windows-waitable-queues/src/slotwise_mpsc.rs +++ b/crates/windows-waitable-queues/src/slotwise_mpsc.rs @@ -193,8 +193,12 @@ pub fn bounded(capacity: usize) -> Result<(Producer, Consumer), Capacit /// /// That avoidance is what distinguishes the two multi-producer shapes, but /// **it is not what makes either one faster**: measurement found this shape the -/// slower of the two under contention, by up to 6.4x. See the crate -/// documentation for the numbers and for how to choose. +/// slower of the two under contention on the hosts tried. See the crate +/// documentation's attributed table for the figures and the conditions they were +/// taken under. (An earlier version of this sentence gave "by up to 6.4x", a +/// figure from a two-host capture withdrawn for predating a correction to the +/// probe's timing window, and pointed at the crate documentation "for how to +/// choose"; which shape suits a deployment is the deployment's question.) /// /// # Errors /// From 9f204bd2d677b167a786209de4ce03d19cc823ad Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 20:01:15 -0400 Subject: [PATCH 037/139] fix(platform-probes): the baseline has no pushes, so the units are per operation `Run` published `nanos_per_push` and `pushes_per_second`, and `render_table` labelled the columns `ns/push` and `pushes/sec`. One row is not pushing anything: `baseline_fetch_add` is N threads incrementing a shared `AtomicU64`, which the probe includes deliberately as the floor for what this processor does to a contended line. It was therefore published with units it does not have, in the report, in the struct's public fields, and in the README column derived from them. Renamed to `nanos_per_op` and `ops_per_second`, with the column headings to match, and the doc comment now says what an operation is in each row rather than leaving a reader to assume every row pushes. The rename is contained: no crate outside this one referenced either field. Also corrects this crate's design note, which said the reservation field "holds four billion outstanding reservations". That is the field's encoding ceiling, not a reachable count -- on `Balanced` the ring capacity binds first, at 2^31 slots on a 64-bit target and 2^30 on a 32-bit one, and a smaller queue binds it sooner. The paragraph immediately below it had already been corrected to draw exactly that distinction, so the two disagreed. Report re-rendered from a release build after the rename; the columns read `ns/op` and `ops/sec` and the table is otherwise unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 8 ++- .../src/queue_contention.rs | 39 ++++++------- .../src/queue_contention/tests.rs | 55 +++++++++---------- 3 files changed, 52 insertions(+), 50 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 17c46d956..6bfec90b1 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -911,8 +911,12 @@ producer. 116M/s is the crate's own disclosed figure and is the honest planning number. **The reservation half is where the bits are being spent, and the trade it makes -is a real one.** The field currently holds four billion outstanding reservations. -Narrowing it is what buys the position bits: 2^21 reservations leaves about a +is a real one.** The field currently *encodes* up to four billion outstanding +reservations, which is a field ceiling rather than a reachable count: on +`Balanced` the ring's capacity binds first (at most 2^31 slots on a 64-bit +target, 2^30 on a 32-bit one), and a smaller queue binds it sooner still. +Narrowing the field is what buys the position bits: 2^21 reservations leaves +about a day, 2^12 leaves over a year, and 2^8 leaves twenty years. The last reaches the same practical headroom a 128-bit word gives, on a plain `AtomicU64`, without a third-party dependency and without reopening `D-18`'s i686 question. diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 228c06649..22d3b8516 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -165,10 +165,17 @@ pub struct Run { pub shape: &'static str, /// How many producer threads pushed concurrently. pub producers: usize, - /// Median nanoseconds per successful push, across all producers. - pub nanos_per_push: f64, - /// Successful pushes per second, summed across producers. - pub pushes_per_second: f64, + /// Median nanoseconds per successful operation, across all producers. + /// + /// An *operation* is one successful push for every queue shape. For + /// [`shapes::BASELINE_FETCH_ADD`] it is one `fetch_add` on a shared + /// `AtomicU64` -- that row is a floor rather than a queue, so it has no + /// pushes to report, and labelling this field per-push would publish it with + /// units it does not have. + pub nanos_per_op: f64, + /// Successful operations per second, summed across producers. See + /// [`Run::nanos_per_op`] for what counts as an operation in each row. + pub ops_per_second: f64, /// Pushes refused for want of room during the median run. /// /// Non-zero means the run was at least partly bounded by the consumer @@ -218,7 +225,7 @@ impl Observation { pub fn scaling(&self, regime: &[Run], shape: &str, producers: usize) -> Option { let one = self.find(regime, shape, 1)?; let many = self.find(regime, shape, producers)?; - Some(many.pushes_per_second / one.pushes_per_second) + Some(many.ops_per_second / one.ops_per_second) } } @@ -233,13 +240,13 @@ pub fn render_table(out: &mut dyn fmt::Write, runs: &[Run]) { let _ = writeln!( out, "{:<18} {:>10} {:>14} {:>16} {:>14}", - "shape", "producers", "ns/push", "pushes/sec", "refusals" + "shape", "producers", "ns/op", "ops/sec", "refusals" ); for run in runs { let _ = writeln!( out, "{:<18} {:>10} {:>14.1} {:>16.0} {:>14}", - run.shape, run.producers, run.nanos_per_push, run.pushes_per_second, run.refusals + run.shape, run.producers, run.nanos_per_op, run.ops_per_second, run.refusals ); } } @@ -268,23 +275,17 @@ pub fn format_scaling(scaling: Option) -> String { #[must_use] pub fn format_ratio(numerator: Option, denominator: Option) -> String { match (numerator, denominator) { - (Some(numerator), Some(denominator)) if denominator.nanos_per_push > 0.0 => { - format!( - "{:.2}x", - numerator.nanos_per_push / denominator.nanos_per_push - ) + (Some(numerator), Some(denominator)) if denominator.nanos_per_op > 0.0 => { + format!("{:.2}x", numerator.nanos_per_op / denominator.nanos_per_op) } _ => "--".to_owned(), } } -/// One row's nanoseconds per push, or `--` when the row is missing. +/// One row's nanoseconds per operation, or `--` when the row is missing. #[must_use] pub fn format_nanos(run: Option) -> String { - run.map_or_else( - || "--".to_owned(), - |run| format!("{:.1}", run.nanos_per_push), - ) + run.map_or_else(|| "--".to_owned(), |run| format!("{:.1}", run.nanos_per_op)) } /// Time every configuration. @@ -395,8 +396,8 @@ fn median_run( Run { shape, producers, - nanos_per_push: elapsed_nanos / pushes, - pushes_per_second: pushes / (elapsed_nanos / 1e9), + nanos_per_op: elapsed_nanos / pushes, + ops_per_second: pushes / (elapsed_nanos / 1e9), refusals, } } diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 5977e144c..26d549fc0 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -9,16 +9,16 @@ use super::*; use std::time::Duration; /// A `Run` with everything but the fields under test held constant. -fn run(shape: &'static str, producers: usize, pushes_per_second: f64) -> Run { +fn run(shape: &'static str, producers: usize, ops_per_second: f64) -> Run { Run { shape, producers, - nanos_per_push: if pushes_per_second > 0.0 { - 1_000_000_000.0 / pushes_per_second + nanos_per_op: if ops_per_second > 0.0 { + 1_000_000_000.0 / ops_per_second } else { 0.0 }, - pushes_per_second, + ops_per_second, refusals: 0, } } @@ -66,7 +66,7 @@ fn find_distinguishes_rows_that_share_a_shape() { let four = observed .find(&observed.isolated, shapes::RESERVING_MPSC, 4) .expect("present"); - assert_ne!(one.pushes_per_second, four.pushes_per_second); + assert_ne!(one.ops_per_second, four.ops_per_second); } #[test] @@ -78,7 +78,7 @@ fn find_distinguishes_rows_that_share_a_producer_count() { let slotwise = observed .find(&observed.isolated, shapes::SLOTWISE_MPSC, 1) .expect("present"); - assert_ne!(reserving.pushes_per_second, slotwise.pushes_per_second); + assert_ne!(reserving.ops_per_second, slotwise.ops_per_second); } #[test] @@ -111,7 +111,7 @@ fn find_reads_only_the_regime_it_is_given() { let drained = observed .find(&observed.drained, shapes::RESERVING_MPSC, 1) .expect("present in drained"); - assert_ne!(isolated.pushes_per_second, drained.pushes_per_second); + assert_ne!(isolated.ops_per_second, drained.ops_per_second); assert!( observed .find(&observed.drained, shapes::SLOTWISE_MPSC, 1) @@ -372,7 +372,7 @@ fn format_scaling_renders_a_finite_value_and_marks_everything_else() { fn format_ratio_divides_and_guards_its_denominator() { let fast = run(shapes::RESERVING_MPSC, 4, 200_000_000.0); let slow = run(shapes::SLOTWISE_MPSC, 4, 100_000_000.0); - // slow is 10.0 ns/push, fast is 5.0, so fast/slow is 0.50x. + // slow is 10.0 ns/op, fast is 5.0, so fast/slow is 0.50x. assert_eq!(format_ratio(Some(fast), Some(slow)), "0.50x"); assert_eq!(format_ratio(Some(slow), Some(fast)), "2.00x"); assert_eq!(format_ratio(None, Some(slow)), "--"); @@ -386,10 +386,7 @@ fn format_ratio_divides_and_guards_its_denominator() { fn format_ratio_refuses_a_zero_denominator() { let measured = run(shapes::RESERVING_MPSC, 4, 100_000_000.0); let absent = run(shapes::SLOTWISE_MPSC, 4, 0.0); - assert_eq!( - absent.nanos_per_push, 0.0, - "the fixture must have zero cost" - ); + assert_eq!(absent.nanos_per_op, 0.0, "the fixture must have zero cost"); assert_eq!(format_ratio(Some(measured), Some(absent)), "--"); } @@ -412,12 +409,12 @@ fn render_table_writes_a_header_and_one_line_per_run() { render_table(&mut out, &rows); let lines: Vec<&str> = out.lines().collect(); assert_eq!(lines.len(), 3, "a header and two rows, got {out:?}"); - assert!(lines[0].contains("shape") && lines[0].contains("ns/push")); + assert!(lines[0].contains("shape") && lines[0].contains("ns/op")); assert!(lines[1].contains(shapes::BASELINE_FETCH_ADD)); assert!(lines[2].contains(shapes::RESERVING_MPSC)); assert!( lines[2].contains("10.0"), - "100M pushes/sec is 10.0 ns/push, got {:?}", + "100M ops/sec is 10.0 ns/op, got {:?}", lines[2] ); } @@ -469,11 +466,11 @@ fn scripted_repetitions() -> Vec { #[test] fn median_run_reports_the_median_repetition_rather_than_the_first_or_last() { let measured = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); - // 3e6 ns over 1 * 50,000 pushes is 60 ns per push. + // 3e6 ns over 1 * 50,000 pushes is 60 ns per op. assert!( - (measured.nanos_per_push - 60.0).abs() < 1e-9, - "expected the 3e6 ns median, got {} ns/push", - measured.nanos_per_push + (measured.nanos_per_op - 60.0).abs() < 1e-9, + "expected the 3e6 ns median, got {} ns/op", + measured.nanos_per_op ); assert_eq!(measured.shape, shapes::RESERVING_MPSC); assert_eq!(measured.producers, 1); @@ -505,9 +502,9 @@ fn median_run_discards_the_warmup_pass() { "the warmup's refusals were reported" ); assert!( - measured.nanos_per_push < 100.0, - "the warmup's 999e6 ns reached the report as {} ns/push", - measured.nanos_per_push + measured.nanos_per_op < 100.0, + "the warmup's 999e6 ns reached the report as {} ns/op", + measured.nanos_per_op ); } @@ -518,12 +515,12 @@ fn median_run_discards_the_warmup_pass() { #[test] fn median_run_derives_both_rates_from_the_one_chosen_repetition() { let measured = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); - let round_trip = 1_000_000_000.0 / measured.nanos_per_push; + let round_trip = 1_000_000_000.0 / measured.nanos_per_op; assert!( - (measured.pushes_per_second - round_trip).abs() < 1e-3, - "{} pushes/sec does not agree with {} ns/push", - measured.pushes_per_second, - measured.nanos_per_push + (measured.ops_per_second - round_trip).abs() < 1e-3, + "{} ops/sec does not agree with {} ns/op", + measured.ops_per_second, + measured.nanos_per_op ); } @@ -534,10 +531,10 @@ fn median_run_divides_the_median_by_every_producers_pushes() { let one = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); let four = median_run(shapes::RESERVING_MPSC, 4, scripted(scripted_repetitions())); assert!( - (one.nanos_per_push / four.nanos_per_push - 4.0).abs() < 1e-9, + (one.nanos_per_op / four.nanos_per_op - 4.0).abs() < 1e-9, "four producers push four times as many items in the same span: {} vs {}", - one.nanos_per_push, - four.nanos_per_push + one.nanos_per_op, + four.nanos_per_op ); } From c846922eab749e14c7eaa3f1dac4d881fe37d63a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 20:01:41 -0400 Subject: [PATCH 038/139] docs(waitable-queues): withdraw the control-band conclusion, which six sites made The review named one site. The claim was in six, and it is false at all of them. D-41 and five other places said the deeper layouts differ from `Balanced` at high producer counts "by less than the run-to-run variation of the same code measured twice". The probe note they cite says the opposite in terms: at sixteen and thirty-two producers the re-apportionments do NOT sit inside the control. The figures are 1.23-1.30x against a control that itself reaches 1.12x, and the note's own conclusion is that this is "a flag, not a finding". So the crate was publishing a stronger claim than its cited source, in the direction of reassurance, six times over. All six now carry the qualified reading: outside the control, but too close to it to establish an ordering or a cost on this host. Swept by matching the proposition rather than the phrasing, since the six sites used three different wordings. The three recurrence-horizon tables (README, crate rustdoc, `ClaimLayout`) gave 37 seconds / 28 days / 20 years under a column headed "At sustained maximum rate". That column is arithmetic over a 116M pushes/s premise which predates the timing-window correction -- so it was a pre-correction figure presented as current. Not recomputed, because the horizon a caller needs is the one on their own hardware; instead the column is relabelled as the pre-correction planning rate and each table now says which way the correction moves it. The correction lowers the true rate and lengthens the horizons, so the published figures remain a floor: they say the wrap arrives sooner than it does, which is the conservative direction for a hazard. D-27 still presented the sequence-line mechanism as the explanation for D-26's result, after `reserving_mpsc`'s module docs withdrew exactly that attribution as unestablished. The section is now marked as the historical argument, keeping the end-to-end observation and the padding experiment that rejects false sharing, and stating that the probe times the complete push and cannot isolate the read the explanation rests on. `ClaimLayout`'s rustdoc said the two halves "are not equally valuable, and the shipping default spends the bits on the less valuable one". That ranks the fields for the caller, which D-41 and D-no-client-prescriptions both refuse, and it was a leftover of the withdrawn producer-count premise -- the half looked less valuable only because its ceiling looked unreachable. Rewritten as a trade whose sides matter differently by deployment. The README's measurement table is relabelled per operation to match the probe's corrected units, since its `baseline_fetch_add` column is a `fetch_add` rather than a push. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-waitable-queues/DESIGN-NOTES.md | 12 ++++++++- crates/windows-waitable-queues/README.md | 25 ++++++++++++------- crates/windows-waitable-queues/src/lib.rs | 17 ++++++++----- .../src/reserving_mpsc.rs | 22 ++++++++++------ 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index 398a398d0..eafff6b27 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts differ by less than the run-to-run variation of the same code measured twice -- which is one host declining to call it, not a cost. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: a 32-bit field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, where the bound that matters is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host: a flag to measure locally, not a finding. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: a 32-bit field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, where the bound that matters is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered @@ -912,6 +912,16 @@ single consumer rather than the claim. ## D-27: why, and why it is not a bug to fix +**The causal mechanism below is a hypothesis this workspace has not established, and the figures in it +predate a correction to the probe's timing window.** Read the whole section as the historical argument +that made `D-26`'s result explicable rather than as a measured finding. What survives is the end-to-end +observation -- `reserving_mpsc` measured faster than `slotwise_mpsc` under contention on the hosts tried +-- and the fact that the difference is a property of the two *protocols* rather than of two +implementations of one. What does not survive is the attribution: the probe times the complete push and +so cannot isolate or bound the sequence read, which is the quantity this explanation rests on. The +padding experiment below is a real measurement and still rejects the false-sharing hypothesis; its +numbers, being pre-correction, should be read as optimistic. + The obvious response to D-26 is that `slotwise_mpsc` must have a defect. It does not, and the difference is worth understanding because it is a property of the two *protocols* rather than of two implementations of one. diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index e882e4a1a..05658f991 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -125,13 +125,21 @@ what it costs is reservations held simultaneously: `Producer::reserve` takes many as the field allows, and a caller that holds many at once is choosing against the narrower layouts rather than against a producer count. -| Layout | Reservation-count field ceiling | Pushes to recurrence | At sustained maximum rate | +| Layout | Reservation-count field ceiling | Pushes to recurrence | At the pre-correction planning rate | |---|---|---|---| | `Balanced` (default) | 4,294,967,295 | 2^32 | about 37 seconds | | `Enduring` | 65,535 | 2^48 | about 28 days | | `Perpetual` | 255 | 2^56 | about 20 years | | `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | +The last column is arithmetic, not a measurement: pushes-to-recurrence divided by +a sustained rate of about 116 million pushes per second. **That rate predates a +correction to the probe's timing window**, which had overstated throughput -- so +the true sustained rate is lower and these horizons longer. They are kept as a +floor, saying the wrap arrives sooner than it does, which is the conservative +direction for a hazard. The horizon that matters is the one on your hardware at +your rate. + The middle column is the field's ceiling, not a reachable number of reservations: admission is also bounded by capacity -- `reserve` refuses once the ring has no room beyond the reservations already outstanding -- so the achievable count is @@ -152,9 +160,7 @@ let (tx, rx) = reserving_mpsc::bounded_as::(64)?; `Enduring`, and `Perpetual` all issue the same exchange on the same 64-bit word and differ only in shift and mask constants, so there is no structural reason for one to be slower -- but **what that costs in throughput is not established**: a -probe comparing them found them indistinguishable at low producer counts, and at -high counts a difference that did not clearly exceed the run-to-run variation of -the same code measured twice. `Wide` is a separate matter: it needs a 128-bit exchange, +probe comparing them found them indistinguishable at low producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. `Wide` is a separate matter: it needs a 128-bit exchange, and the whole push path was measured as slower under it as producer count rises -- near parity at one or two, several times by thirty-two, in the isolated regime -- and it is the only thing in @@ -196,9 +202,7 @@ proportionally longer to reach its wrap. - **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty years out. **What it costs in throughput is not established** -- it issues the same atomic compare-exchange on the same `u64` as the default, and was measured - as indistinguishable from it at low producer counts; at - high counts the difference did not clearly exceed the run-to-run variation of - the same code measured twice. + as indistinguishable from it at low producer counts; at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. - **`slotwise_mpsc` does not have this hazard** under any layout. Its positions are 64 bits on every target, so the equivalent wrap needs 2^64 claims. It does not offer `Reserving`. @@ -388,8 +392,11 @@ The measurements below are one host's observation, recorded with the parameters that produced them. They are not a ranking, and which shape suits a given deployment is the deployment's question. -**What was measured**, in ns per push, isolated regime (producers only, capacity -large enough that nothing is refused), median of three runs: +**What was measured**, in ns per operation, isolated regime (producers only, +capacity large enough that nothing is refused), median of three runs. An +operation is one successful push for the three queue shapes; for +`baseline_fetch_add` it is one `fetch_add`, which is why the column is labelled +per operation rather than per push: | producers | `slotwise_mpsc` | `reserving_mpsc` | `permit_mpsc` | `baseline_fetch_add` | |---|---|---|---|---| diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 19151d4ad..22261b8ee 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -94,13 +94,21 @@ //! holds many at once is choosing against the narrower layouts rather than //! against a producer count. //! -//! | Layout | Reservation-count field ceiling | Pushes to recurrence | At sustained maximum rate | +//! | Layout | Reservation-count field ceiling | Pushes to recurrence | At the pre-correction planning rate | //! |---|---|---|---| //! | `Balanced` (default) | 4,294,967,295 | 2^32 | about 37 seconds | //! | `Enduring` | 65,535 | 2^48 | about 28 days | //! | `Perpetual` | 255 | 2^56 | about 20 years | //! | `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | //! +//! The last column is arithmetic, not a measurement: pushes-to-recurrence +//! divided by a sustained rate of about 116 million pushes per second. **That +//! rate predates a correction to the probe's timing window**, which had +//! overstated throughput -- so the true sustained rate is lower and these +//! horizons longer. They are kept as a floor, saying the wrap arrives sooner +//! than it does, which is the conservative direction for a hazard. The horizon +//! that matters is the one on your hardware at your rate. +//! //! The reservation-count column is the field's ceiling, not a reachable number of //! reservations: admission is also bounded by capacity, so the achievable count //! is the lesser of the two. For `Balanced` the capacity bound binds first -- @@ -121,8 +129,7 @@ //! word and differ only in shift and mask constants, so there is no structural //! reason for one to be slower -- but **what that costs in throughput is not //! established**: a probe comparing them found them indistinguishable at low -//! producer counts, and at high counts a difference that did not clearly exceed -//! the run-to-run variation of the same code measured twice. `Wide` is a separate +//! producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. `Wide` is a separate //! matter: it needs a 128-bit exchange, and the whole push path was measured as //! slower under it as producer count rises -- near parity at one or two, //! several times by thirty-two, in the isolated regime -- and it is the only @@ -169,9 +176,7 @@ //! - **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty //! years out. **What it costs in throughput is not established** -- it issues //! the same atomic compare-exchange on the same `u64` as the default, and was -//! measured as indistinguishable from it at low producer counts; at high -//! counts the difference did not clearly exceed the run-to-run variation of -//! the same code measured twice. +//! measured as indistinguishable from it at low producer counts; at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. //! - **[`slotwise_mpsc`] does not have this hazard** under any layout. Its //! positions are 64 bits on every target, so the equivalent wrap needs 2^64 //! claims. It does not offer [`Reserving`]. diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 4799e7a9d..0e4fc78c7 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -191,15 +191,17 @@ use crate::options::Options; /// them is therefore a trade, and this trait is where a caller chooses which /// side to spend them on. /// -/// **The two things being traded are not equally valuable, and the shipping -/// default spends the bits on the less valuable one.** The reservation count +/// **What the two halves buy is different in kind, and which matters depends on +/// the deployment.** The reservation count /// bounds how many messages may be held in flight at once. That bound is the /// lesser of the ring's capacity and the layout's count field, and it is /// reachable by a *single* producer: [`Producer::reserve`] takes `&self` and /// returns an owned [`Reservation`], so one thread can hold as many as the /// field allows. (An earlier version of this paragraph said the practical bound -/// was "the number of producers mid-send, so hundreds or thousands". That was -/// wrong, and it mattered -- it made the narrower fields look unreachable. One +/// was "the number of producers mid-send, so hundreds or thousands", and called +/// the reservation half "the less valuable one" on that basis. The bound was +/// wrong, and it mattered -- it made the narrower fields look unreachable, which +/// is what made the trade look one-sided. One /// producer alone fills `Perpetual`'s 255 and is then refused, which /// `one_producer_alone_can_exhaust_the_reservation_field` pins.) The position /// decides @@ -233,13 +235,19 @@ use crate::options::Options; /// note quotes; a queue that must drain cannot sustain the fastest rate /// measured, so treat these as a floor on time rather than a forecast. /// +/// **That rate premise predates a correction to the probe's timing window**, +/// which had overstated throughput. The correction therefore moves the true +/// sustained rate *down* and these horizons *up*, so the figures above remain a +/// floor -- they say the wrap arrives sooner than it does, which is the +/// conservative direction for a hazard. They have not been recomputed, because +/// the horizon a caller needs is the one on their own hardware and at their own +/// rate; the arithmetic is field width divided by rate. +/// /// **Choosing a deeper position is the same instruction on the same word.** All /// three issue the same atomic compare-exchange on the same `u64` and differ /// shift and mask constants, so there is no structural reason for one to be /// slower. **What that costs in throughput is not established**: a probe -/// comparing them found them indistinguishable at low producer counts, and at -/// high counts a difference that did not clearly exceed the run-to-run -/// variation of the same code measured twice. The settled trade is the +/// comparing them found them indistinguishable at low producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. The settled trade is the /// reservation ceiling; throughput is target-dependent and this crate does not /// characterise it beyond the one host in the note above. /// From fecd3523df6cc4d660d0598ec22db6aa21ce7865 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 18:29:36 -0700 Subject: [PATCH 039/139] fix(platform-probes): carry the dispersion the crate's own contract requires `median_run` took five timed repetitions, sorted them, kept the middle one and threw the other four away. `Run` then carried a median and nothing else, so the report published a figure with no statement of how much it moves. This crate's own D-observations-not-verdicts says every published figure is recorded "with the number of runs *and their dispersion*", and that a ratio quoted without those "is an anecdote, not data a reader can compare against their own hardware". The probe was the source of the figures that decision governs and did not satisfy it. The four discarded repetitions were the only evidence it had about its stability within a run. `Run` gains `fastest_nanos_per_op` and `slowest_nanos_per_op`, taken from the ends of the sort that already existed, plus a `spread()` that reports the ratio and returns zero rather than infinity when a shape failed to run -- the same guard `format_ratio` carries for the same reason. `render_table` publishes both as an `ns/op range` column and a `spread` column. Nine tests cover them: the range is the whole sample rather than two adjacent repetitions, the median lies inside it, the warmup is excluded from the range as well as from the median, the spread is slowest-over-fastest, an identical sample spreads to exactly 1.00x, a zero sample is zero rather than infinite, and the renderer emits both columns. Verified load-bearing by sabotage: taking the fastest from the median index instead of the minimum fails `median_run_carries_the_fastest_and_slowest_repetitions`. The dispersion is worth having on its own terms, not only for the contract. In the capture taken with it, `slotwise_mpsc` at two producers spans 19.3 to 59.5 ns/op *within one configuration on one host* -- a factor of three that the median alone concealed entirely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/queue_contention.rs | 58 ++++++++- .../src/queue_contention/tests.rs | 119 ++++++++++++++++++ 2 files changed, 173 insertions(+), 4 deletions(-) diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 22d3b8516..522081e8f 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -182,6 +182,40 @@ pub struct Run { /// rather than by the claim, which is a fact about the measurement and not /// about the queue. pub refusals: u64, + /// Fastest of the [`REPETITIONS`] timed repetitions, in nanoseconds per + /// operation. + /// + /// Carried because [`d-observations-not-verdicts`] obliges every published + /// figure to arrive with its run count *and its dispersion*: a median alone + /// is an anecdote a reader cannot compare against their own hardware. The + /// four repetitions the median discards are the only evidence this probe has + /// about its own stability within a run, and discarding them silently was + /// the crate publishing a figure its own contract forbids. + /// + /// [`d-observations-not-verdicts`]: https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/DESIGN-NOTES.md#d-observations-not-verdicts + pub fastest_nanos_per_op: f64, + /// Slowest of the [`REPETITIONS`] timed repetitions, in nanoseconds per + /// operation. See [`Run::fastest_nanos_per_op`]. + pub slowest_nanos_per_op: f64, +} + +impl Run { + /// The spread across this configuration's repetitions, as a multiple. + /// + /// `1.00` would mean every repetition took the same time. A wide spread + /// says the figure beside it is one draw from a distribution this host does + /// not hold still, which is the reading the median alone hides. + /// + /// Zero when the fastest repetition took no measurable time, which cannot + /// happen for a real run and is reported rather than divided by. + #[must_use] + pub fn spread(&self) -> f64 { + if self.fastest_nanos_per_op > 0.0 { + self.slowest_nanos_per_op / self.fastest_nanos_per_op + } else { + 0.0 + } + } } /// Everything one invocation measured. @@ -239,14 +273,23 @@ impl Observation { pub fn render_table(out: &mut dyn fmt::Write, runs: &[Run]) { let _ = writeln!( out, - "{:<18} {:>10} {:>14} {:>16} {:>14}", - "shape", "producers", "ns/op", "ops/sec", "refusals" + "{:<18} {:>10} {:>14} {:>16} {:>14} {:>18} {:>9}", + "shape", "producers", "ns/op", "ops/sec", "refusals", "ns/op range", "spread" ); for run in runs { let _ = writeln!( out, - "{:<18} {:>10} {:>14.1} {:>16.0} {:>14}", - run.shape, run.producers, run.nanos_per_op, run.ops_per_second, run.refusals + "{:<18} {:>10} {:>14.1} {:>16.0} {:>14} {:>18} {:>9}", + run.shape, + run.producers, + run.nanos_per_op, + run.ops_per_second, + run.refusals, + format!( + "{:.1}-{:.1}", + run.fastest_nanos_per_op, run.slowest_nanos_per_op + ), + format_scaling(Some(run.spread())), ); } } @@ -391,6 +434,11 @@ fn median_run( let mut results: Vec = (0..REPETITIONS).map(|_| timer(producers)).collect(); results.sort_by(|left, right| left.0.total_cmp(&right.0)); let (elapsed_nanos, refusals) = results[REPETITIONS / 2]; + // The sort is ascending by elapsed time, so the extremes are the ends. They + // are carried rather than discarded because a median without its dispersion + // is what `d-observations-not-verdicts` forbids publishing. + let (fastest_nanos, _) = results[0]; + let (slowest_nanos, _) = results[REPETITIONS - 1]; let pushes = (producers * PUSHES_PER_PRODUCER) as f64; Run { @@ -399,6 +447,8 @@ fn median_run( nanos_per_op: elapsed_nanos / pushes, ops_per_second: pushes / (elapsed_nanos / 1e9), refusals, + fastest_nanos_per_op: fastest_nanos / pushes, + slowest_nanos_per_op: slowest_nanos / pushes, } } diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 26d549fc0..3cbd23732 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -20,6 +20,16 @@ fn run(shape: &'static str, producers: usize, ops_per_second: f64) -> Run { }, ops_per_second, refusals: 0, + fastest_nanos_per_op: if ops_per_second > 0.0 { + 1_000_000_000.0 / ops_per_second + } else { + 0.0 + }, + slowest_nanos_per_op: if ops_per_second > 0.0 { + 1_000_000_000.0 / ops_per_second + } else { + 0.0 + }, } } @@ -559,3 +569,112 @@ fn render_table_shows_a_nonzero_refusal_count() { "the refusal column is unlabelled" ); } + +/// The dispersion `d-observations-not-verdicts` obliges the crate to publish. +/// `median_run` sorts ascending, so the extremes are the ends of that sort -- +/// these pin that the reported range is the whole sample rather than, say, the +/// median repeated or two adjacent repetitions. +#[test] +fn median_run_carries_the_fastest_and_slowest_repetitions() { + let measured = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); + // Scripted timed repetitions are 1e6..5e6 ns over 50,000 pushes: 20..100 ns/op. + assert!( + (measured.fastest_nanos_per_op - 20.0).abs() < 1e-9, + "expected the 1e6 ns repetition as fastest, got {} ns/op", + measured.fastest_nanos_per_op + ); + assert!( + (measured.slowest_nanos_per_op - 100.0).abs() < 1e-9, + "expected the 5e6 ns repetition as slowest, got {} ns/op", + measured.slowest_nanos_per_op + ); +} + +/// The median must lie inside the range, or the two are describing different +/// samples. This is the cheap invariant that catches a range computed from the +/// wrong vector or from an unsorted one. +#[test] +fn median_run_brackets_its_median_with_the_range() { + let measured = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); + assert!( + measured.fastest_nanos_per_op <= measured.nanos_per_op, + "fastest {} must not exceed the median {}", + measured.fastest_nanos_per_op, + measured.nanos_per_op + ); + assert!( + measured.nanos_per_op <= measured.slowest_nanos_per_op, + "median {} must not exceed the slowest {}", + measured.nanos_per_op, + measured.slowest_nanos_per_op + ); +} + +/// The warmup is excluded from the dispersion as well as from the median. Its +/// scripted 999e6 ns would otherwise dominate the range and make every row look +/// wildly unstable. +#[test] +fn median_run_excludes_the_warmup_from_the_range() { + let measured = median_run(shapes::RESERVING_MPSC, 1, scripted(scripted_repetitions())); + assert!( + measured.slowest_nanos_per_op < 1_000.0, + "the warmup's 999e6 ns reached the range as {} ns/op", + measured.slowest_nanos_per_op + ); +} + +#[test] +fn spread_is_the_slowest_over_the_fastest() { + let mut run = run(shapes::RESERVING_MPSC, 4, 100_000_000.0); + run.fastest_nanos_per_op = 10.0; + run.slowest_nanos_per_op = 13.0; + assert!((run.spread() - 1.3).abs() < 1e-9, "got {}", run.spread()); +} + +/// A configuration whose repetitions all took the same time has a spread of +/// exactly one, which is what "this host held still" looks like. +#[test] +fn spread_of_an_identical_sample_is_one() { + let mut run = run(shapes::RESERVING_MPSC, 4, 100_000_000.0); + run.fastest_nanos_per_op = 42.0; + run.slowest_nanos_per_op = 42.0; + assert!((run.spread() - 1.0).abs() < 1e-9, "got {}", run.spread()); +} + +/// A shape that failed to run reports zero, and dividing by it would put `inf` +/// in a column a reader takes for a measurement -- the same guard +/// `format_ratio` carries. +#[test] +fn spread_of_a_zero_sample_is_zero_rather_than_infinite() { + let mut run = run(shapes::SLOTWISE_MPSC, 4, 0.0); + run.fastest_nanos_per_op = 0.0; + run.slowest_nanos_per_op = 0.0; + assert_eq!(run.spread(), 0.0); + assert!( + run.spread().is_finite(), + "the spread must never be infinite" + ); +} + +#[test] +fn render_table_publishes_the_range_and_the_spread() { + let mut row = run(shapes::RESERVING_MPSC, 8, 100_000_000.0); + row.fastest_nanos_per_op = 9.5; + row.slowest_nanos_per_op = 12.5; + let mut out = String::new(); + render_table(&mut out, &[row]); + let header = out.lines().next().expect("a header"); + let line = out.lines().nth(1).expect("one row"); + assert!( + header.contains("range") && header.contains("spread"), + "the dispersion columns are unlabelled: {header:?}" + ); + assert!( + line.contains("9.5-12.5"), + "the range is missing from {line:?}" + ); + assert!( + line.contains("1.32x"), + "the spread is missing from {line:?}" + ); +} From 3062c474a52dfb84aec0a14e02a9b644f4742852 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 18:29:36 -0700 Subject: [PATCH 040/139] chore(topology): the placement tool is placement-probe, not probe-core-affinity Ride-along from a rename sweep; no behaviour change, so no release. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-topology-sys/DESIGN-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/windows-topology-sys/DESIGN-NOTES.md b/crates/windows-topology-sys/DESIGN-NOTES.md index 3138f0229..47e95a31c 100644 --- a/crates/windows-topology-sys/DESIGN-NOTES.md +++ b/crates/windows-topology-sys/DESIGN-NOTES.md @@ -54,7 +54,7 @@ additional CPU and memory cost; do not solve the consumer's architecture for the Three ways to obtain a `MachineMemoryTopology` are supported on purpose, and the crate's own front page advertises the third: "deserialize one from JSON written for a machine you do not have". That is a feature -- it is how a consumer tests against hardware it lacks, and this workspace needs it right now, because -`probe-core-affinity` must exercise NUMA selection logic on hosts that have exactly one NUMA node. +`placement-probe` must exercise NUMA selection logic on hosts that have exactly one NUMA node. The hazard is that **the resulting value looked exactly like a discovered one**. There is a passing test in this crate that parses a *Linux-shaped* description, complete with an ACPI SLIT-style distance From 81cca99f36be09a879f6404020d3d6e36c08f614 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 18:30:01 -0700 Subject: [PATCH 041/139] docs(waitable-queues): publish the dispersion, and stop naming a tool that does not exist The measurement table published medians alone, which D-observations-not-verdicts forbids: it obliges every figure to carry its run count AND its dispersion. Recaptured with the probe's new range support -- three fresh whole-probe runs -- and every cell now shows the median of the three followed by the full range across all fifteen repetitions those runs contain. The ranges turn out to be the more useful half. `slotwise_mpsc` at two producers spans 19.3 to 59.5 ns/op, a factor of three within one configuration on one host, and at thirty-two spans 131.4 to 268.3. A reader given only the median would have taken these for settled numbers. The between-run sentence is updated to the new capture's per-run medians (225.7, 218.0, 192.9 at sixteen producers) and now also gives the within-run span, since the two answer different questions. `probe-core-affinity` does not exist. The binary is `placement-probe`, declared in windows-placement-probe's manifest; the old name was real once and the rename never swept. The README told a reader to run it, which was unfollowable. Found in six places across three files rather than the one reported, and the queue crate's own notes already used the new name once, so the file disagreed with itself. Two decision-index rows still asserted what their sections had already withdrawn -- the same miss as D-17 an earlier round. D-26 gave "up to 4x" with no qualification while D-29 says that capture's magnitudes do not survive the timing correction; D-27 asserted the sequence-protocol mechanism as current while its own section is marked historical. Both rows now carry the status adjacent to the claim, which is where the repository convention puts it. `reserving_mpsc`'s module docs said the consumer-position read "is not what makes either shape slower". That is a causal NEGATIVE, and the probe cannot support it for exactly the reason the paragraph below it already gave: the measurement times the complete push, so the read is one term among protocol, metadata and retry costs and is never separated from them. The honest statement is that the extra read did not stop this shape being the faster of the two here. Withdrawing the positive mechanism last round and leaving the negative standing was the same premise-removed-conclusion-kept error this sweep has now hit twice. `slotwise_mpsc` carried the withdrawn mechanism in an internal comment that the last sweep missed because it searched rustdoc and markdown. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-waitable-queues/DESIGN-NOTES.md | 12 ++--- crates/windows-waitable-queues/README.md | 46 ++++++++++++------- .../src/reserving_mpsc.rs | 16 +++++-- .../src/slotwise_mpsc.rs | 11 +++-- 4 files changed, 53 insertions(+), 32 deletions(-) diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index eafff6b27..a67f189e8 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -60,10 +60,10 @@ preferred. | D-23 | **High-water tracking is opt-in at construction; refusals and doorbell rings are always on.** The difference is where each can be paid for: refusals sit on the failure path and rings on a path that already costs a syscall, but a peak has to observe *every* change -- and on `slotwise_mpsc` that means the producer reading the consumer's position, the shared line [D-16](#d-16) built a separate shape to avoid. Untracked reports `None`, not `0`. | | D-24 | **Counting the doorbell's rings turns the skip optimisation into part of the observable contract, and that is the point rather than a side effect.** R9 asks for the count precisely so "disabling the skip must change the number" -- so the sabotage entry for removing the skip changed from a control expecting `survives` to a defect expecting `caught`. An optimisation nobody can measure is an assumption. | | D-25 | **`Observable` deliberately does not restate depth.** [D-2](#d-2)'s sketch listed it, but `Bounded::len` already reports it from positions the queue keeps anyway. Naming it twice would give one number two spellings and two places to drift. What belongs on `Observable` is only what must be *accumulated*. | -| D-26 | **Measured: the tail claim contends badly, and `reserving_mpsc` is up to 4x FASTER than `slotwise_mpsc` under contention -- the opposite of what [D-16](#d-16) assumed.** Aggregate throughput *falls* as producers are added, for both shapes and far more than a bare contended atomic explains. D-16's premise, that reading the consumer's position makes the reserving shape the expensive one, is falsified everywhere except a single producer with a live consumer. | -| D-27 | **The gap is intrinsic to Vyukov's sequence protocol, not a fixable flaw in `slotwise_mpsc`'s retry loop.** Its producer must read a slot's sequence *before* claiming, and that slot marches through memory as the tail advances while other producers write it. Padding slots onto their own cache lines was tested and rejected: it recovers about a fifth at eight producers, for four times the memory, and leaves the shape still 2.8x slower. | +| D-26 | **Measured: the tail claim contends badly, and `reserving_mpsc` is FASTER than `slotwise_mpsc` under contention -- the opposite of what [D-16](#d-16) assumed.** Aggregate throughput *falls* as producers are added, for both shapes and far more than a bare contended atomic explains. D-16's premise, that reading the consumer's position makes the reserving shape the expensive one, is falsified everywhere except a single producer with a live consumer. **The magnitude originally stated here -- "up to 4x" -- predates a correction to the probe's timing window and is withdrawn**; per [D-29](#d-29) no current public figure derives from that capture. The direction survives the correction; the magnitude does not. | +| D-27 | **Mechanism not established; see the section, which is marked historical.** **Originally: the gap is intrinsic to Vyukov's sequence protocol, not a fixable flaw in `slotwise_mpsc`'s retry loop.** Its producer must read a slot's sequence *before* claiming, and that slot marches through memory as the tail advances while other producers write it. Padding slots onto their own cache lines was tested and rejected: it recovers about a fifth at eight producers, for four times the memory, and leaves the shape still 2.8x slower. | | D-28 | **Amended -- the blanket rejection is withdrawn; the verdict depends on thread placement, and the open question is an open question queued outside this crate.** Caching the peer's index was measured, and it engaged as designed. It cost ~1.8x on x64 with the threads across cores, and *won* 17x on ARM64 and 1.8x on x64 SMT siblings. Batch depth decides the sign, and batch depth is set by where the two threads are scheduled -- not by the architecture and not by our code. A prefetch-only "warming" control changed nothing on any host. | -| D-29 | **Both multi-producer shapes ship. The crate publishes what it measured and declines to choose for the caller.** [D-26](#d-26) falsified [D-16](#d-16)'s cost premise, which reopened merge-or-delete; the answer is neither. Vyukov's sequence protocol and the head-based one are independently researched designs, both in production use, and our own workload having settled which *we* want is not evidence about anyone else's. Deleting a shape because no visible consumer wants it is what PLATFORM INTEGRITY forbids. What the crate owes instead is the data and, through `probe-core-affinity`, the means to gather it on the caller's own hardware. | +| D-29 | **Both multi-producer shapes ship. The crate publishes what it measured and declines to choose for the caller.** [D-26](#d-26) falsified [D-16](#d-16)'s cost premise, which reopened merge-or-delete; the answer is neither. Vyukov's sequence protocol and the head-based one are independently researched designs, both in production use, and our own workload having settled which *we* want is not evidence about anyone else's. Deleting a shape because no visible consumer wants it is what PLATFORM INTEGRITY forbids. What the crate owes instead is the data and, through `placement-probe`, the means to gather it on the caller's own hardware. | | D-30 | **Both MPSC shapes are qualified by name; neither is `mpsc`.** A bare `mpsc` beside `reserving_mpsc` makes one canonical by implication, which contradicts this crate's own "no shape is the canonical one" and, after [D-29](#d-29), is simply false. `slotwise_mpsc` names its claim protocol -- it claims slot by slot, with no shared counter -- and avoids the reading `sequence_mpsc` invites, that it alone preserves FIFO order when both shapes do. Renamed before first publish, where it is free. | | D-31 | **0.1.0 ships without machine-checked memory orderings, and says so in its own documentation.** Model-checking gates 1.0, not 0.1.0. It would close the *demonstrated* gap -- a weakened `Acquire` survives the whole suite -- but not the dangerous one: it cannot model `SetEvent`/`ResetEvent`, so it cannot cover the doorbell, and [D-15](#d-15)'s lost wakeup, the only ordering bug this crate has had, was found by sabotage instead. The risk it addresses is mostly regression risk, which is lowest before there are consumers. The disclosure, not the deferral, is the decision. | | D-32 | **`Reserving::Reservation<'a>` gains a bound, before the crate publishes.** The associated type is currently unbounded, so a caller generic over the trait can claim a slot and drop it but never redeem it -- the trait cannot express the operation it exists for. Both implementors already have identical `send` and `is_disconnected` signatures, so the bound is additive; adding it after publication is a breaking change to every implementor. Done as SH-1.5: the [`Claim`](src/traits.rs) trait carries `send` and `is_disconnected`, and both reservation types implement it as forwarders. `Claim` must be in scope to call those methods on a claim whose concrete type the caller has not named, which is why it is re-exported at the crate root. | @@ -1067,7 +1067,7 @@ The ARM64 host asked the x64 host to test a specific prediction: **that SMT sibl stay in lockstep, giving shallow batches, and that this was the condition making caching lose.** ARM64 has no SMT and physically cannot express that placement, so only the x64 host could answer it. -`probe-core-affinity`, x64, medians of three runs (all three agreed to within 3%): +`placement-probe`, x64, medians of three runs (all three agreed to within 3%): | placement | base ns/item | cached ns/item | cached batch depth | verdict | |---|---|---|---|---| @@ -1118,7 +1118,7 @@ single machine and none cross-checks another. The per-placement coverage matrix, (`same cache, cross class`) that neither host can express, is kept with the open question in that open question rather than duplicated here. -**A probe defect found while doing this, now fixed.** `probe-core-affinity` printed its placement +**A probe defect found while doing this, now fixed.** `placement-probe` printed its placement table from a hard-coded list of four variants that omitted `SameCoreSiblings`, while the interpretation beneath it iterated over the placements actually measured. On an SMT host the table therefore showed the sibling row as absent while the interpretation quoted a number for it -- the @@ -1195,7 +1195,7 @@ What the crate owes a caller instead is honesty and equipment: - **The measurements, stated plainly**, including the regimes where each wins and the fact that the answer inverted once already when a second architecture was tried. -- **The means to measure their own domain.** `probe-core-affinity` and the placement tool exist so a +- **The means to measure their own domain.** `placement-probe` and the placement tool exist so a caller can settle this on their own hardware and workload rather than inheriting ours. A queue library that publishes one benchmark and calls it a recommendation is asserting a conclusion about machines it has never seen. diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 05658f991..a9aded602 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -393,19 +393,30 @@ that produced them. They are not a ranking, and which shape suits a given deployment is the deployment's question. **What was measured**, in ns per operation, isolated regime (producers only, -capacity large enough that nothing is refused), median of three runs. An -operation is one successful push for the three queue shapes; for -`baseline_fetch_add` it is one `fetch_add`, which is why the column is labelled -per operation rather than per push: +capacity large enough that nothing is refused). Each cell is the median of three +whole-probe runs, followed by the full range across all fifteen repetitions those +runs contain. An operation is one successful push for the three queue shapes; +for `baseline_fetch_add` it is one `fetch_add`, which is why the column is +labelled per operation rather than per push: | producers | `slotwise_mpsc` | `reserving_mpsc` | `permit_mpsc` | `baseline_fetch_add` | |---|---|---|---|---| -| 1 | 6.3 | 5.4 | 8.0 | 2.3 | -| 2 | 54.0 | 34.9 | 41.5 | 11.7 | -| 4 | 89.3 | 37.1 | 32.1 | 15.1 | -| 8 | 143.8 | 38.1 | 26.4 | 15.2 | -| 16 | 246.9 | 51.1 | 21.4 | 15.3 | -| 32 | 235.7 | 53.0 | 21.2 | 15.1 | +| 1 | 6.3 (6.3-7.5) | 5.4 (5.4-6.4) | 7.9 (7.9-8.3) | 2.3 (2.3-2.7) | +| 2 | 50.6 (19.3-59.5) | 31.9 (22.5-35.2) | 44.2 (37.4-45.9) | 12.1 (5.8-14.3) | +| 4 | 91.6 (89.7-99.9) | 37.2 (31.6-41.5) | 31.8 (30.4-32.9) | 13.8 (12.6-17.6) | +| 8 | 138.6 (126.9-157.6) | 37.8 (34.3-41.7) | 25.9 (25.0-27.4) | 14.7 (13.9-15.9) | +| 16 | 218.0 (188.9-272.7) | 47.9 (44.9-56.0) | 21.8 (20.9-25.6) | 14.8 (14.4-15.9) | +| 32 | 224.7 (131.4-268.3) | 51.3 (40.7-55.4) | 21.9 (20.7-39.0) | 15.0 (14.7-15.7) | + +**The ranges are the point, not a footnote.** `slotwise_mpsc` at two producers +spans 19.3 to 59.5 -- a factor of three within one configuration on one host -- +and at thirty-two, 131.4 to 268.3. A median quoted without that is an anecdote, +which is why +[D-observations-not-verdicts](../windows-platform-probes/DESIGN-NOTES.md#d-observations-not-verdicts) +obliges every published figure here to carry its run count *and* its dispersion. +An earlier version of this table published the medians alone and did not meet +that obligation; the probe now carries the range through to the report so it +cannot be omitted again. **Attribution, because a figure without it is not reusable data:** @@ -414,8 +425,8 @@ per operation rather than per push: | Host | `x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16]` | | Profile | release | | Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | -| Runs | 3 whole-probe invocations, median of the three | -| Instrument | `probe-queue-contention`, at commit `a99108f` | +| Runs | 3 whole-probe invocations; cells are the median of the three, ranges span all 15 repetitions | +| Instrument | `probe-queue-contention`, rebuilt for this capture | | Taken | 2026-09-15 | The banner's `numa[16]` is a single NUMA node holding all sixteen processors, so @@ -426,10 +437,11 @@ thing N threads can do to a contended line, included so the queue figures can be read against what this processor does to such a line at all. **Read these as one machine's numbers.** Producer counts above 8 oversubscribe -this host's 8 physical cores, and the spread across the three runs is not small: -`slotwise_mpsc` at sixteen producers gave 257.3, 215.1 and 246.9 across them. The -probe's own same-code control has been measured at 0.68-1.27x over seven runs, -which is wide enough to swallow small differences; see +this host's 8 physical cores, and the spread is not small at either scale. +Between runs: `slotwise_mpsc` at sixteen producers gave whole-run medians of +225.7, 218.0 and 192.9. Within a single run its five repetitions spanned 188.9 to +272.7. The probe's own same-code control has been measured at 0.68-1.27x over +seven runs, which is wide enough to swallow small differences; see [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). That seven-run sweep is a **separate capture** taken to size the noise floor, not a longer version of this table -- its medians differ from the ones above, which is @@ -453,7 +465,7 @@ be the cheaper shape, and measurement said otherwise on both machines. **What moves these numbers.** Producer count, how hard the consumer drains, and where the threads are scheduled all change the answer -- thread placement alone moved an SPSC handoff by 5.6x on an earlier host this workspace measured. The -`probe-core-affinity` tool in this repository runs that measurement, and +`placement-probe` tool in this repository runs that measurement, and `probe-queue-contention` runs the one above. Two things that look like reasons to choose and are not: diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 0e4fc78c7..ca4612495 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -58,11 +58,17 @@ //! on **every** push, including the pushes that never reserve anything -- which //! is what `slotwise_mpsc` avoids and why it cannot offer reservation at all. //! -//! **That cost is not what makes either shape slower.** This one measured -//! *faster* than `slotwise_mpsc` under contention on the hosts tried. The -//! magnitude belongs with the capture that produced it rather than here, so see -//! the crate documentation's attributed table for the figures and the conditions -//! they were taken under. +//! **What that read costs is not established, and this comparison cannot +//! establish it.** This shape measured *faster* than `slotwise_mpsc` under +//! contention on the hosts tried -- an end-to-end result over the complete push +//! path, in which the consumer-position read is one term among the protocol, +//! metadata and retry costs and is not separated from them. So the honest +//! statement is that the extra read did not stop this shape being the faster of +//! the two here; it is **not** that the read is cheap, nor that it is "not what +//! makes either shape slower", which is a causal negative the measurement cannot +//! support either. The magnitude belongs with the capture that produced it +//! rather than here, so see the crate documentation's attributed table for the +//! figures and the conditions they were taken under. //! //! An earlier version of this paragraph gave a figure ("by up to 6.4x") taken //! from a two-host comparison that has since been withdrawn for predating a diff --git a/crates/windows-waitable-queues/src/slotwise_mpsc.rs b/crates/windows-waitable-queues/src/slotwise_mpsc.rs index ddab6187e..401b0fbb9 100644 --- a/crates/windows-waitable-queues/src/slotwise_mpsc.rs +++ b/crates/windows-waitable-queues/src/slotwise_mpsc.rs @@ -551,10 +551,13 @@ impl Producer { // asked for the answer. // // Note what this property does *not* buy: measurement found this shape - // slower than `reserving_mpsc` under contention despite it, because the - // slot sequence a producer must read instead marches through memory - // while other producers write it. Staying off the shared line is why - // the two shapes are different, not why either is quick. + // slower than `reserving_mpsc` under contention despite it. Why is not + // established -- the probe times the complete push, so the sequence read + // is one term among several and is never isolated. An earlier version of + // this comment attributed it to the slot sequence marching through + // memory while other producers write it; that mechanism is plausible and + // unmeasured. Staying off the shared line is why the two shapes are + // different, not a claim about which is quick. // // Off, the cost is one predictable branch on a field written once at // construction, so the line is shared but read-only -- the cheap kind. From da20e51e27a6e14273e1189c24d704e5699a96e7 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 18:47:14 -0700 Subject: [PATCH 042/139] docs(platform-probes): record that the dispersion work landed, and how to run it Three sites still described the probe as it was before the previous commit added the range columns. M4.2's gap text said `median_run` "discards the other four" repetitions and that `Run` carries a median with no spread. Both were true when the item was written and false by the time it was read, which makes a checklist item report completed work as pending -- the failure mode the action-only rule exists to prevent, at the other end. The item now records the dispersion work as done and scopes what remains to the sampling controls alone. The binary's own comment said the dispersion "is not yet carried; M4.2 covers it", sitting four lines above the code that now emits it. The crate's entry-point documentation lists a run command per binary probe and had none for `probe-queue-contention`, so a reader following the documented entry points had no way to run the probe this branch adds. Added, with the --release requirement in the comment rather than left to be discovered: a debug build reports the two shapes as equivalent, which is a confident wrong answer rather than a merely imprecise one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 20 +++++++++++-------- .../src/bin/queue_contention.rs | 5 +++-- crates/windows-platform-probes/src/lib.rs | 5 +++++ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index c6bd40cc9..87592e0a3 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -137,14 +137,18 @@ correctness in the archive. is only interpretable with its capture parameters, and these are now among them. Making the sampling adjustable without recording it would turn one reproducibility problem into a worse one. - **Emit the dispersion, not just the median.** `median_run` currently sorts the five repetitions, - keeps the middle one, and **discards the other four** -- so `Run` carries a median with no spread, - and the ranges published in [DESIGN-NOTES.md](DESIGN-NOTES.md) exist only because they were - computed by hand outside the probe. That is the same contract failure from the other side: the - decision above requires a figure to carry "the number of runs with their dispersion", and the - instrument does not supply it. Keep at least the min and max alongside the median, and render - them. Reported by review, and correctly -- the same-code control is the evidence a reader needs - to judge any ratio here, and it is exactly what is being thrown away. + **Emit the dispersion, not just the median. -- DONE, landed in this branch.** `median_run` used to + sort the five repetitions, keep the middle one and **discard the other four**, so `Run` carried a + median with no spread and any range published elsewhere had been computed by hand outside the + probe. `Run` now carries `fastest_nanos_per_op` and `slowest_nanos_per_op` alongside the median, + with a `spread()` accessor, and `render_table` publishes both an `ns/op range` and a `spread` + column. Reported by review, and correctly -- it was the same contract failure from the other side, + since the decision above requires a figure to carry "the number of runs with their dispersion" and + the instrument did not supply it. + + **What remains in this item is the sampling controls only** -- making `PUSHES_PER_PRODUCER`, + `REPETITIONS` and the producer counts settable, and recording whatever was used beside the + figures. **Not in scope:** deciding why the control is wide. That is the judgement this tooling supports, and per the design note a negative result -- "lengthening and repeating do not narrow it, so the diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 96754a525..65dc61033 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -56,8 +56,9 @@ fn render(out: &mut dyn std::fmt::Write) { // interpretable with them -- see D-observations-not-verdicts. The build // profile is one of them too: a captured report has to be able to show it // was produced by a build that can measure, not merely stay silent when it - // was. The dispersion belongs here as well and is not yet carried; M4.2 - // covers it. + // was. The dispersion belongs here as well, and now is: each row carries the + // range across its repetitions and the resulting spread. What M4.2 still + // covers is making the sampling parameters settable rather than fixed. let _ = writeln!( out, "profile: {}", diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index 5e74ed70f..1acfb291f 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -96,6 +96,11 @@ //! cargo test -p windows-platform-probes -- --include-ignored # both tiers //! cargo test -p windows-platform-probes -- --ignored # ignored tier only //! cargo run -p windows-platform-probes --bin probe-cancel-io # binary only +//! +//! # binary only, and --release is not optional: a debug build reports +//! # slotwise_mpsc and reserving_mpsc as equivalent, which is a confident +//! # wrong answer rather than a merely imprecise one. Takes about a minute. +//! cargo run --release -p windows-platform-probes --bin probe-queue-contention //! ``` //! //! `--include-ignored` is what CI runs, and is almost always what a human From fca9a9c396234484cfd6ebe2a04c605c3a43bf2a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 18:47:35 -0700 Subject: [PATCH 043/139] docs(waitable-queues): the rustdoc table still held the superseded capture Recapturing the measurement table last commit updated the README and left the crate rustdoc's copy of the same table untouched, so the two published different numbers for the same host and instrument. That is the two-copies hazard this crate already documents for its memory-orderings disclosure, arriving in the table beside it -- and I created it by updating one copy, one commit after writing that updating one copy is the hazard. The rustdoc table now carries the same medians and ranges as the README, the same attribution, and the same statement of what a range spans. One claim in both copies was false and is the more serious of the two. The prose said `slotwise_mpsc` at sixteen producers "spanned 188.9 to 272.7" *within a single run*. Those endpoints are the aggregate across all fifteen repetitions of three runs, not one run's five -- I wrote a within-run claim from a between-run number without checking it. Re-measured to state it correctly: a separate invocation of the same build gives that row a median of 226.5 over a 181.5-242.3 range, a spread of 1.33x. Both copies now distinguish the between-run medians, the within-run spread, and the aggregate range the table publishes, because they answer different questions and I had collapsed them. The attribution said the instrument was "rebuilt for this capture", which names no revision a reader can return to -- a regression from the `a99108f` it replaced. Both copies now cite `fecd352`, the commit that added the range columns, verified to contain them. The recurrence-horizon paragraph called the rate model "that same measurement" and referred to "the fastest rate measured". The table it describes is arithmetic over a rate premise that the preceding paragraph already says predates the timing correction, so calling it a measurement repeats the overclaim that paragraph exists to withdraw. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/README.md | 18 +++++++---- crates/windows-waitable-queues/src/lib.rs | 39 ++++++++++++++++------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index a9aded602..ec88cfa8c 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -192,9 +192,9 @@ accumulated over an uptime. Reaching the wrap is necessary but not sufficient: a producer must also be stalled inside a window a few instructions wide. Rare, but a preemption is enough, and "rare" over billions of pushes is not "never". -The figures in the table above scale that same measurement by the position +The figures in the table above scale that same rate model by the position width, so they are a floor on time rather than a forecast: a queue that must -drain cannot sustain the fastest rate measured, and a slower producer takes +drain cannot sustain the fastest rate shown, and a slower producer takes proportionally longer to reach its wrap. **What bears on it.** @@ -426,7 +426,7 @@ cannot be omitted again. | Profile | release | | Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | | Runs | 3 whole-probe invocations; cells are the median of the three, ranges span all 15 repetitions | -| Instrument | `probe-queue-contention`, rebuilt for this capture | +| Instrument | `probe-queue-contention`, built from `fecd352` (the commit that added the range columns) | | Taken | 2026-09-15 | The banner's `numa[16]` is a single NUMA node holding all sixteen processors, so @@ -438,10 +438,14 @@ read against what this processor does to such a line at all. **Read these as one machine's numbers.** Producer counts above 8 oversubscribe this host's 8 physical cores, and the spread is not small at either scale. -Between runs: `slotwise_mpsc` at sixteen producers gave whole-run medians of -225.7, 218.0 and 192.9. Within a single run its five repetitions spanned 188.9 to -272.7. The probe's own same-code control has been measured at 0.68-1.27x over -seven runs, which is wide enough to swallow small differences; see +*Between* runs: `slotwise_mpsc` at sixteen producers gave whole-run medians of +225.7, 218.0 and 192.9. *Within* a run the probe reports its own per-row spread +-- a fourth, separate invocation of the same build gave that row a median of +226.5 over a 181.5-242.3 range, a spread of 1.33x across its five repetitions. +The parenthesised ranges in the table above are the wider quantity: the extremes +over all fifteen repetitions of the three captured runs. The probe's same-code +control has been measured at 0.68-1.27x over seven runs, which is wide enough to +swallow small differences; see [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). That seven-run sweep is a **separate capture** taken to size the noise floor, not a longer version of this table -- its medians differ from the ones above, which is diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 22261b8ee..1a009b41d 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -314,16 +314,25 @@ //! parameters that produced them. They are not a ranking. //! //! Isolated regime (producers only, capacity large enough that nothing is -//! refused), ns per push, median of three runs: +//! refused), ns per operation. Each cell is the median of three whole-probe +//! runs, followed by the full range across all fifteen repetitions those runs +//! contain -- [`D-observations-not-verdicts`] obliges a published figure to +//! carry its run count *and* its dispersion, and the ranges are the more useful +//! half: `slotwise_mpsc` at two producers spans a factor of three within one +//! configuration on one host. +//! +//! An operation is one successful push for the three queue shapes; for +//! `baseline_fetch_add` it is one `fetch_add`, which is why the column is +//! labelled per operation rather than per push. //! //! | producers | `slotwise_mpsc` | `reserving_mpsc` | `permit_mpsc` | `baseline_fetch_add` | //! |---|---|---|---|---| -//! | 1 | 6.3 | 5.4 | 8.0 | 2.3 | -//! | 2 | 54.0 | 34.9 | 41.5 | 11.7 | -//! | 4 | 89.3 | 37.1 | 32.1 | 15.1 | -//! | 8 | 143.8 | 38.1 | 26.4 | 15.2 | -//! | 16 | 246.9 | 51.1 | 21.4 | 15.3 | -//! | 32 | 235.7 | 53.0 | 21.2 | 15.1 | +//! | 1 | 6.3 (6.3-7.5) | 5.4 (5.4-6.4) | 7.9 (7.9-8.3) | 2.3 (2.3-2.7) | +//! | 2 | 50.6 (19.3-59.5) | 31.9 (22.5-35.2) | 44.2 (37.4-45.9) | 12.1 (5.8-14.3) | +//! | 4 | 91.6 (89.7-99.9) | 37.2 (31.6-41.5) | 31.8 (30.4-32.9) | 13.8 (12.6-17.6) | +//! | 8 | 138.6 (126.9-157.6) | 37.8 (34.3-41.7) | 25.9 (25.0-27.4) | 14.7 (13.9-15.9) | +//! | 16 | 218.0 (188.9-272.7) | 47.9 (44.9-56.0) | 21.8 (20.9-25.6) | 14.8 (14.4-15.9) | +//! | 32 | 224.7 (131.4-268.3) | 51.3 (40.7-55.4) | 21.9 (20.7-39.0) | 15.0 (14.7-15.7) | //! //! Attribution, because a figure without it is not reusable data: //! @@ -332,10 +341,12 @@ //! | Host | `x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16]` | //! | Profile | release | //! | Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | -//! | Runs | 3 whole-probe invocations, median of the three | -//! | Instrument | `probe-queue-contention`, at commit `a99108f` | +//! | Runs | 3 whole-probe invocations; cells are the median of the three, ranges span all 15 repetitions | +//! | Instrument | `probe-queue-contention`, built from `fecd352` | //! | Taken | 2026-09-15 | //! +//! [`D-observations-not-verdicts`]: https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/DESIGN-NOTES.md#d-observations-not-verdicts +//! //! The banner's `numa[16]` is a single NUMA node holding all sixteen processors, //! so nothing here says anything about cross-domain behaviour. `permit_mpsc` is //! behind `experimental-permit-claim` and is not covered by the semver promise. @@ -344,8 +355,14 @@ //! line at all. //! //! **Read these as one machine's numbers.** Producer counts above 8 oversubscribe -//! this host's 8 physical cores, and the spread across the three runs is not -//! small: `slotwise_mpsc` at sixteen producers gave 257.3, 215.1 and 246.9. +//! this host's 8 physical cores, and the spread is not small at either scale. +//! *Between* runs: `slotwise_mpsc` at sixteen producers gave whole-run medians +//! of 225.7, 218.0 and 192.9. *Within* a run the probe reports its own per-row +//! spread -- a fourth, separate invocation of the same build gave that row a +//! median of 226.5 over a 181.5-242.3 range, a spread of 1.33x across its five +//! repetitions. The parenthesised ranges in the table above are the wider +//! quantity: the extremes over all fifteen repetitions of the three captured +//! runs. //! //! A previous version of this table compared an AMD EPYC 7763 slice against a //! Snapdragon X2 Elite. It was removed rather than carried forward: its figures From 0416f0f7345b2ffd725d83319f4d0471c53bb8ab Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 19:43:19 -0700 Subject: [PATCH 044/139] fix(platform-probes): the refusal counts do not rule the tail out The report told a reader that "a run with many refusals was waiting for the consumer, not for the tail". The second half is a causal negative the counter cannot support. A refusal proves a producer met a full queue, so the consumer is one term in what that row measured; it says nothing about whether the tail claim was also binding, and both can bind at once. The probe's own notes record these counts as unstable and as not settling that comparison, so the report was making a claim the instrument disclaims two files away. Rewritten to say what the counter shows and what it leaves open. Verified by rebuilding and reading the rendered caveat rather than the source. This is the third causal negative found in this branch, after "that cost is not what makes either shape slower" and the mechanism attribution before it. The shape is consistent enough to name: withdrawing a positive claim leaves its negation looking like the safe residue, and it is not -- "X is not the cause" needs the same evidence as "X is the cause", which is exactly what a whole-path measurement cannot supply. Also corrects the root design note's claim about what a table-versus-constants check would catch, for the second time in the same paragraph. It named the 2^31/2^30 target-dependent capacity and the MAX_RESERVED-as-capacity conflation as examples such a check would have caught. Neither is in any table: the layout table's columns are the layout, the reservation-count field ceiling, the pushes-to-recurrence count and a time, and both named errors are prose assertions in the surrounding text. The corrected bound is more useful than the overstatement it replaces. Roughly half the restatements that note measures are tabular and mechanically checkable; the other half are prose claims ABOUT those constants and need something that reads assertions rather than rows. A remedy covering the first half is still worth having. Claiming it covers both is how a partial instrument comes to be trusted as a complete one -- which is the failure that note exists to warn about, committed inside the paragraph proposing the remedy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 20 ++++++++++++++----- .../src/bin/queue_contention.rs | 10 +++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 1f2c3599c..1920aa6d7 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1950,11 +1950,21 @@ conversation despite fixing different things. `windows-waitable-queues` (`#[doc = include_str!]` in [lib.rs](crates/windows-waitable-queues/src/lib.rs)), so a test can parse the published layout tables and assert every row against `ClaimLayout`'s constants -- converting 19 hand-written `255`s -into one definition and N checked derivations, with no generator and no new tooling. Mechanically, -that would have caught the `2^31`/`2^30` error, the `MAX_RESERVED`-as-capacity conflation, and the -ceiling and push-count columns of both stale recurrence tables. The time columns need the assumed -rate pinned somewhere single before they can be checked the same way, which is a second and smaller -piece of work rather than a reason not to do the first. +into one definition and N checked derivations, with no generator and no new tooling. + +**Be precise about what that would and would not catch, because this paragraph has now overstated it +twice.** The layout table's columns are the layout name, the reservation-count field ceiling, the +pushes-to-recurrence count, and a time. A constants check covers the **ceiling and push-count +columns** outright. The time column additionally needs the assumed rate pinned somewhere single. And +the two errors this note originally named -- the `2^31`/`2^30` target-dependent capacity and the +`MAX_RESERVED`-as-capacity conflation -- it would **not** have caught at all: both are prose +assertions in the surrounding text, not cells in any table. + +That bound is the useful part rather than a caveat on it. Roughly half the restatements measured +above are tabular and mechanically checkable; the other half are prose claims *about* those +constants, and catching those needs something that reads assertions rather than rows. A remedy that +covers the first half is still worth having, and claiming it covers both is how a partial instrument +comes to be trusted as a complete one. **No work is scheduled by this note.** It was written to inform a decision that has not been taken, and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 65dc61033..d94c4c19e 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -430,6 +430,14 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " many refusals was waiting for the consumer, not for the tail." + " many refusals met a full queue often, so the consumer is one term" + ); + let _ = writeln!( + out, + " in what it measured. That does not rule the tail out -- both can" + ); + let _ = writeln!( + out, + " bind at once, and these counts do not separate them." ); } From cddef16ff59b03bb26300d805b552dfbfc90c3e2 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 19:43:19 -0700 Subject: [PATCH 045/139] docs(waitable-queues): D-41 still called the reservation half the wrong one to spend bits on That is a layout recommendation, which D-no-client-prescriptions refuses: the probe withdrew the cost-free claim rather than establishing a winner. The same phrasing was corrected in ClaimLayout's rustdoc a round earlier -- it read 'the half worth least' there -- and D-41 was missed because that sweep matched the rustdoc's wording rather than the proposition. Rewritten as the trade it is: a reservation field whose bound is the lesser of ring capacity and field width, against a position whose width sets the recurrence horizon, with which one a deployment needs left to the deployment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/DESIGN-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index a67f189e8..cd3de2ed0 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host: a flag to measure locally, not a finding. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). The reservation half was the wrong half to spend bits on: a 32-bit field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, where the bound that matters is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host: a flag to measure locally, not a finding. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). Which half the bits go to is the trade this decision exposes rather than settles: a 32-bit reservation field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, against a position whose width sets the recurrence horizon. The bound that matters on the reservation side is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. Which of the two a deployment needs is the deployment's question. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered From bd82bf95f5a156ad1f655dd25b47cc9cc4eaae1f Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 20:06:40 -0700 Subject: [PATCH 046/139] docs(platform-probes): split M4.2's completed half out as M4.5 and archive it M4.2 was unchecked while embedding a completed sub-deliverable marked DONE, plus its review history -- a checklist entry that is simultaneously pending and done, which the action-only rule and the checked-means-done rule both forbid. The dispersion work is now M4.5, checked, with its body in COMPLETED-CHECKLIST.md behind an anchored heading and a one-line stub in its place. M4.2 keeps only the sampling controls, which is what the binary's own comment already says it covers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 17 +++-------- .../COMPLETED-CHECKLIST.md | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 87592e0a3..205b1b683 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -137,23 +137,14 @@ correctness in the archive. is only interpretable with its capture parameters, and these are now among them. Making the sampling adjustable without recording it would turn one reproducibility problem into a worse one. - **Emit the dispersion, not just the median. -- DONE, landed in this branch.** `median_run` used to - sort the five repetitions, keep the middle one and **discard the other four**, so `Run` carried a - median with no spread and any range published elsewhere had been computed by hand outside the - probe. `Run` now carries `fastest_nanos_per_op` and `slowest_nanos_per_op` alongside the median, - with a `spread()` accessor, and `render_table` publishes both an `ns/op range` and a `spread` - column. Reported by review, and correctly -- it was the same contract failure from the other side, - since the decision above requires a figure to carry "the number of runs with their dispersion" and - the instrument did not supply it. - - **What remains in this item is the sampling controls only** -- making `PUSHES_PER_PRODUCER`, - `REPETITIONS` and the producer counts settable, and recording whatever was used beside the - figures. - **Not in scope:** deciding why the control is wide. That is the judgement this tooling supports, and per the design note a negative result -- "lengthening and repeating do not narrow it, so the floor is here" -- is a real answer that gets recorded beside the figures. + **Also not in scope, because it is done:** emitting the dispersion. See M4.5 below. + +- [x] **M4.5** -- Emit the dispersion, not just the median. -> [completed 2026-09-15](COMPLETED-CHECKLIST.md#m45) + - [ ] **M4.3** -- Close the undrained window at the start of the drained regime with a readiness handshake, and re-measure everything that changes. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 93044d433..93a02252a 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1114,3 +1114,33 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. currently has no sentence about claims. > **-> DEPENDS ON M2.14.1:** the pointer has nothing to point at until the manifest exists. + +## Moved 2026-09-15 -- M4.5: the probe carries its own dispersion + +### M4.5 -- Emit the dispersion, not just the median. *(completed 2026-09-15 20:04:09 UTC-07:00)* + +Split out of M4.2 when it landed, because M4.2's remaining work -- the sampling controls -- is +independent of it and an unchecked item must not embed a completed deliverable. + +`median_run` took the five timed repetitions, sorted them, kept the middle one and **discarded the +other four**. `Run` then carried a median with no spread, so every figure derived from the probe was +published without dispersion and any range quoted elsewhere had been computed by hand outside the +instrument. + +That was a contract failure rather than a gap in polish. +[D-observations-not-verdicts](DESIGN-NOTES.md#d-observations-not-verdicts) requires every published +figure to carry "the number of runs with their dispersion", and says a ratio quoted without those +"is an anecdote, not data a reader can compare against their own hardware". The probe was the source +of the figures that decision governs and did not satisfy it. Reported by review, and correctly. + +`Run` gained `fastest_nanos_per_op` and `slowest_nanos_per_op`, taken from the ends of the sort that +already existed, plus a `spread()` accessor that returns zero rather than infinity when a shape +failed to run -- the same guard `format_ratio` carries for the same reason. `render_table` publishes +an `ns/op range` column and a `spread` column. + +Nine tests cover it, verified load-bearing by sabotage: taking the fastest from the median index +instead of the minimum fails `median_run_carries_the_fastest_and_slowest_repetitions`. + +The dispersion justified itself on first capture. `slotwise_mpsc` at two producers spans 19.3 to +59.5 ns/op -- a factor of three within one configuration on one host -- which the median alone had +concealed entirely, in a table that had already been published twice. From bc56465296998a51edb9228e55e6b0dd36e5bba7 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 20:07:03 -0700 Subject: [PATCH 047/139] docs(waitable-queues): the horizons were still "measured" in seven places Qualifying the layout table's rate premise as pre-correction left every prose restatement of the same horizons unqualified, so the crate published a withdrawn measurement as current in the paragraphs a reader is most likely to meet first. The review named three sites. There were seven, in three different wordings: README.md "the exposure, measured rather than estimated" "at this crate's own measured rates" src/lib.rs the same paragraph, duplicated reserving_mpsc.rs module header: "at this crate's measured rates" Balanced: "37 seconds of sustained maximum-rate pushing" Enduring: "28 days of sustained maximum-rate pushing" Perpetual: "20 years of sustained maximum-rate pushing" permit_mpsc.rs "laps in minutes at this crate's measured rates" slotwise_mpsc.rs "at this crate's measured rates is a matter of minutes" The last two are in shapes the review did not look at, and they matter for the same reason: both use the rate to argue a soundness property about 32-bit counters, so the figure is load-bearing there rather than incidental. All seven now say "disclosed" rather than "measured" and name the rate as a floor -- the correction lowers the true rate and lengthens the horizon, so these figures say the wrap arrives sooner than it does, which is the conservative direction for a hazard. The full explanation stays in ONE place, ClaimLayout's rustdoc and the two published tables; the layout rustdocs carry a pointer rather than a fourth copy, since restating it at every site is how this drifted in the first place. "Measured rather than estimated" was the worst of them: it makes the provenance the point of the sentence, and the provenance is what changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/README.md | 12 ++++++++---- crates/windows-waitable-queues/src/lib.rs | 11 ++++++++--- crates/windows-waitable-queues/src/permit_mpsc.rs | 3 ++- .../windows-waitable-queues/src/reserving_mpsc.rs | 13 ++++++++----- crates/windows-waitable-queues/src/slotwise_mpsc.rs | 3 ++- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index ec88cfa8c..2fcc786b3 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -184,10 +184,14 @@ receives a different item than the one that was sent, and nothing observable says so -- which is why this is documented here rather than left to a caller to discover, and why it cannot be mitigated after the fact. -**The exposure, measured rather than estimated.** Under `Balanced`, 2^32 pushes -is 37 seconds to roughly four minutes of *sustained* pushing at this crate's own -measured rates -- about two minutes at two producers, which is the smallest -count that can trigger it at all. That is sustained throughput, not a total +**The exposure, as arithmetic over a disclosed rate.** Under `Balanced`, 2^32 +pushes is 37 seconds to roughly four minutes of *sustained* pushing at this +crate's disclosed rates -- about two minutes at two producers, which is the +smallest count that can trigger it at all. **Those rates predate a correction to +the probe's timing window** and are kept as a floor for the reason the layout +table above gives: the correction lowers the rate and lengthens the horizon, so +these figures say the wrap arrives sooner than it does, which is the +conservative direction for a hazard. That is sustained throughput, not a total accumulated over an uptime. Reaching the wrap is necessary but not sufficient: a producer must also be stalled inside a window a few instructions wide. Rare, but a preemption is enough, and "rare" over billions of pushes is not "never". diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 1a009b41d..3d2bc3bb6 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -157,10 +157,15 @@ //! observable says so -- which is why this is documented here rather than left //! to a caller to discover, and why it cannot be mitigated after the fact. //! -//! **The exposure, measured rather than estimated.** Under `Balanced`, 2^32 +//! **The exposure, as arithmetic over a disclosed rate.** Under `Balanced`, 2^32 //! pushes is 37 seconds to roughly four minutes of *sustained* pushing at this -//! crate's own measured rates -- about two minutes at two producers, which is -//! the smallest count that can trigger it at all. That is sustained throughput, +//! crate's disclosed rates -- about two minutes at two producers, which is +//! the smallest count that can trigger it at all. **Those rates predate a +//! correction to the probe's timing window** and are kept as a floor for the +//! reason the layout table above gives: the correction lowers the rate and +//! lengthens the horizon, so these figures say the wrap arrives sooner than it +//! does, which is the conservative direction for a hazard. That is sustained +//! throughput, //! not a total accumulated over an uptime. Reaching the wrap is necessary but //! not sufficient: a producer must also be stalled inside a window a few //! instructions wide. Rare, but a preemption is enough, and "rare" over diff --git a/crates/windows-waitable-queues/src/permit_mpsc.rs b/crates/windows-waitable-queues/src/permit_mpsc.rs index d078ab044..3f01ca07c 100644 --- a/crates/windows-waitable-queues/src/permit_mpsc.rs +++ b/crates/windows-waitable-queues/src/permit_mpsc.rs @@ -78,7 +78,8 @@ use crate::metrics::Metrics; /// /// **64 bits on every target, deliberately, rather than `usize`**, for the same /// reason [`slotwise_mpsc`](crate::slotwise_mpsc) made the same choice: a -/// 32-bit counter laps in minutes at this crate's measured rates, and a shape +/// 32-bit counter laps in minutes at this crate's disclosed rates (a floor, +/// since they predate a timing correction that lowers them), and a shape /// whose soundness depends on the target's pointer width is not one this crate /// ships twice over. /// diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index ca4612495..1204c9665 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -21,8 +21,11 @@ //! panic, or counter reports it. //! //! Under `Balanced`, 2^32 pushes is 37 seconds to about four minutes of -//! *sustained* pushing at this crate's measured rates, roughly two minutes at -//! two producers. The wrap alone is not enough -- a producer must also stall +//! *sustained* pushing at this crate's disclosed rates, roughly two minutes at +//! two producers. Those rates predate a correction to the probe's timing window, +//! so they are a floor rather than a forecast -- the correction lowers the rate +//! and lengthens the horizon, which is the conservative direction for a hazard; +//! see [`ClaimLayout`]. The wrap alone is not enough -- a producer must also stall //! inside a window a few instructions wide -- but a preemption suffices. //! //! **[`ClaimLayout`] is how far away that is.** [`Perpetual`] moves it to 2^56 @@ -543,7 +546,7 @@ impl ClaimWord for u128 { /// reservations whatever the field could encode. It /// recurs after 2^32 pushes -- /// about -/// **37 seconds** of sustained maximum-rate pushing. Past that point, with two +/// **37 seconds** at the pre-correction planning rate ([ClaimLayout] says why\n/// that is a floor). Past that point, with two /// or more producers, the queue can **silently lose an item**: that is the whole /// of the `SH-14.1` exposure, and this layout carries it. /// @@ -569,7 +572,7 @@ impl ClaimLayout for Balanced { /// A deeper position: 16 bits of reservations, 48 of position. /// /// Holds 65,535 outstanding reservations and recurs after 2^48 pushes -- about -/// **28 days** of sustained maximum-rate pushing. +/// **28 days** at the pre-correction planning rate; see [ClaimLayout]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Enduring; impl sealed::Sealed for Enduring {} @@ -582,7 +585,7 @@ impl ClaimLayout for Enduring { /// The deepest position: 8 bits of reservations, 56 of position. /// /// Holds 255 outstanding reservations and recurs after 2^56 pushes -- about -/// **20 years** of sustained maximum-rate pushing, which puts the recurrence +/// **20 years** at the pre-correction planning rate ([ClaimLayout]), which puts the recurrence /// beyond any real deployment rather than merely far away. /// /// 255 reservations is the whole of the trade, and it is a real limit rather diff --git a/crates/windows-waitable-queues/src/slotwise_mpsc.rs b/crates/windows-waitable-queues/src/slotwise_mpsc.rs index 401b0fbb9..a465acc59 100644 --- a/crates/windows-waitable-queues/src/slotwise_mpsc.rs +++ b/crates/windows-waitable-queues/src/slotwise_mpsc.rs @@ -84,7 +84,8 @@ use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; /// counter cannot lap. /// /// With `usize` it can. On a 32-bit target the counter laps after 2^32 claims, -/// which at this crate's measured rates is a matter of minutes: the stalled +/// which at this crate's disclosed rates is a matter of minutes -- a floor, +/// since those rates predate a timing correction that lowers them: the stalled /// producer then sees the same tail bits, succeeds, and writes a slot that has /// since been refilled from the previous lap of the ring. Every other guard in /// this shape holds -- the position really is claimed by exactly one producer; From 0f36dfad5923331a85dd8a2836ef1f0247a692ea Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 20:14:15 -0700 Subject: [PATCH 048/139] fix(waitable-queues): restore backticks and a newline PowerShell ate The previous commit made three edits through a PowerShell double-quoted string containing backtick-delimited intra-doc links. PowerShell treats backtick as its escape character, so [\ClaimLayout\] became [ClaimLayout] at three sites and an intended newline landed as a literal backslash-n at one of them, gluing two doc-comment lines together. Caught by CI's encoding sanity check, which flags a doc-comment marker glued to code -- the one gate in this repository that looks for exactly this. Not caught by clippy, rustdoc with -D warnings, or the test suite, all of which passed on the damaged file: the links still resolved because rustdoc accepts an unbackticked path, and the glued line was still a valid comment. This repository's own instructions say to use the edit tool for anything containing backticks, for this precise reason. Re-fixed with it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/src/reserving_mpsc.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 1204c9665..bbcabe160 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -546,7 +546,8 @@ impl ClaimWord for u128 { /// reservations whatever the field could encode. It /// recurs after 2^32 pushes -- /// about -/// **37 seconds** at the pre-correction planning rate ([ClaimLayout] says why\n/// that is a floor). Past that point, with two +/// **37 seconds** at the pre-correction planning rate ([`ClaimLayout`] says why +/// that is a floor). Past that point, with two /// or more producers, the queue can **silently lose an item**: that is the whole /// of the `SH-14.1` exposure, and this layout carries it. /// @@ -572,7 +573,7 @@ impl ClaimLayout for Balanced { /// A deeper position: 16 bits of reservations, 48 of position. /// /// Holds 65,535 outstanding reservations and recurs after 2^48 pushes -- about -/// **28 days** at the pre-correction planning rate; see [ClaimLayout]. +/// **28 days** at the pre-correction planning rate; see [`ClaimLayout`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Enduring; impl sealed::Sealed for Enduring {} @@ -585,7 +586,7 @@ impl ClaimLayout for Enduring { /// The deepest position: 8 bits of reservations, 56 of position. /// /// Holds 255 outstanding reservations and recurs after 2^56 pushes -- about -/// **20 years** at the pre-correction planning rate ([ClaimLayout]), which puts the recurrence +/// **20 years** at the pre-correction planning rate ([`ClaimLayout`]), which puts the recurrence /// beyond any real deployment rather than merely far away. /// /// 255 reservations is the whole of the trade, and it is a real limit rather From 2039307e3768b5bb9e04118a517dac425998c0c2 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 20:18:53 -0700 Subject: [PATCH 049/139] docs: a design note should not record which branch a checklist item is on The note said M30 is 'not yet on main -- it is pending in PR 92'. That is branch state in a durable document: true when written, false the moment 92 merges, and nothing would have caused anyone to revisit it. The reference is now to M30 in the root checklist, with the M23 renumber kept as history rather than as status, since the old number appears in the commit history and in 92's discussion and a reader meeting it deserves the explanation. Swept the class rather than the line: no other durable document in the repository encodes branch or pull-request status. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 1920aa6d7..f28c077ac 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1969,7 +1969,9 @@ comes to be trusted as a complete one. **No work is scheduled by this note.** It was written to inform a decision that has not been taken, and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule rather than an oversight. The formal-methods survey it bears on is `M30` in the root -[CHECKLIST.md](CHECKLIST.md), which is not yet on `main` -- it is pending in PR #92, where the -milestone was originally numbered `M23` until that ID was found to collide with an archived one. If -the table-versus-constants test or a prose-reduction pass is adopted, each needs its own item at -that time. +[CHECKLIST.md](CHECKLIST.md). If the table-versus-constants test or a prose-reduction pass is +adopted, each needs its own item at that time. + +*(`M30` was authored as `M23` and renumbered when that ID was found to collide with an archived +milestone. Recorded because the old number appears in the commit history and in PR #92's earlier +discussion, not because either number is in doubt now.)* From 32cb97a3a7d0d26fd316b0dcb28dccbf5e5bdbec Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 20:30:21 -0700 Subject: [PATCH 050/139] fix(platform-probes): the derived ratios had no dispersion either Adding the range columns fixed the rows and left every figure DERIVED from them a bare point estimate. The scaling table and the interpretation ratios are exactly where a reader is most likely to over-read a number, and they were the figures on the page with the least stated uncertainty. D-observations-not-verdicts does not distinguish measured from derived: every published figure carries its dispersion. A ratio printed alone, above rows that each carry a range, puts the least certain number on the page in the most confident dress. What the probe can honestly supply is a BOUND, not a distribution, and the difference is stated rather than glossed. Each configuration is measured in its own pass, so the repetitions behind a numerator and a denominator are not paired -- there is no set of per-repetition ratios to take a range over. What follows from the data is that if one cost lies in [a, b] and another in [c, d], their ratio cannot fall outside [a/d, b/c]. That is the widest the ratio could be, which errs in the direction that matters. `ratio_bounds`, `Observation::scaling_bounds`, `format_ratio_bounded` and `format_scaling_bounded` compute and render it; the bound is bracketed to mark it as arithmetic over two spans rather than an observed range. Pairing the repetitions would give a real distribution and is a change to how the probe MEASURES rather than how it reports -- that is M4.4's interleaving, and the rustdoc says so rather than leaving the limitation implicit. Nine tests, including the invariant that makes the bound worth printing: the point estimate must lie inside it. Verified by sabotage -- pairing same-side extremes instead of opposing ones fails `ratio_bounds_pairs_opposing_extremes`. The first rendered row argues the change better than this message can. Scaling at one producer is 1.00x by construction -- a shape against itself -- and its bound is [0.84-1.20]. That uncertainty was always in the data; only the point estimate was ever shown. Also fixes the warmup comment, which said every timer builds and drops its own queue. True of the queue timers and not of `time_contended_atomic`, which allocates one AtomicU64 and a barrier. Both the original claim and this correction were found by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 36 ++-- .../src/queue_contention.rs | 114 ++++++++++++- .../src/queue_contention/tests.rs | 156 ++++++++++++++++++ 3 files changed, 281 insertions(+), 25 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index d94c4c19e..7a19bda00 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -14,7 +14,7 @@ use windows_platform_probes::queue_contention::{ DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, format_nanos, - format_ratio, format_scaling, measure, render_table, shapes, + format_ratio_bounded, format_scaling_bounded, measure, render_table, shapes, }; use windows_platform_probes::report::emit_report; @@ -118,23 +118,23 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " {:<18} {:>12} {:>12} {:>12} {:>14}", + " {:<12} {:>22} {:>22} {:>22} {:>22}", "producers", "slotwise", "reserving", "permit", "atomic floor" ); for &producers in PRODUCER_COUNTS { - let mpsc = observation.scaling(&observation.isolated, shapes::SLOTWISE_MPSC, producers); - let reserving = - observation.scaling(&observation.isolated, shapes::RESERVING_MPSC, producers); - let permit = observation.scaling(&observation.isolated, shapes::PERMIT_MPSC, producers); - let floor = - observation.scaling(&observation.isolated, shapes::BASELINE_FETCH_ADD, producers); + let cell = |shape: &str| { + format_scaling_bounded( + observation.scaling(&observation.isolated, shape, producers), + observation.scaling_bounds(&observation.isolated, shape, producers), + ) + }; let _ = writeln!( out, - " {producers:<18} {:>12} {:>12} {:>12} {:>14}", - format_scaling(mpsc), - format_scaling(reserving), - format_scaling(permit), - format_scaling(floor) + " {producers:<12} {:>22} {:>22} {:>22} {:>22}", + cell(shapes::SLOTWISE_MPSC), + cell(shapes::RESERVING_MPSC), + cell(shapes::PERMIT_MPSC), + cell(shapes::BASELINE_FETCH_ADD) ); } let _ = writeln!( @@ -192,11 +192,11 @@ fn render(out: &mut dyn std::fmt::Write) { let plain = observation.find(&observation.drained, shapes::SLOTWISE_MPSC, producers); let reserving = observation.find(&observation.drained, shapes::RESERVING_MPSC, producers); let permit = observation.find(&observation.drained, shapes::PERMIT_MPSC, producers); - let ratio = format_ratio(reserving, plain); + let ratio = format_ratio_bounded(reserving, plain); // The column SH-15.5 exists to fill: the experimental claim against the // shipping shape it would replace. Below 1.00 means the permit claim is // cheaper; above means removing the room-decision race costs throughput. - let permit_ratio = format_ratio(permit, reserving); + let permit_ratio = format_ratio_bounded(permit, reserving); let _ = writeln!( out, " {producers:<18} {:>14} {:>14} {:>10} {:>14} {:>16}", @@ -372,9 +372,9 @@ fn render(out: &mut dyn std::fmt::Write) { format_nanos(deep), format_nanos(perpetual), format_nanos(wide), - format_ratio(deep, narrow), - format_ratio(perpetual, narrow), - format_ratio(wide, narrow) + format_ratio_bounded(deep, narrow), + format_ratio_bounded(perpetual, narrow), + format_ratio_bounded(wide, narrow) ); } let _ = writeln!(out); diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 522081e8f..3cdfccacf 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -261,6 +261,102 @@ impl Observation { let many = self.find(regime, shape, producers)?; Some(many.ops_per_second / one.ops_per_second) } + + /// The interval [`Observation::scaling`] could occupy, given the two rows' + /// observed spans. + /// + /// **This is a bound, not a sampled distribution, and the difference + /// matters.** The probe measures each configuration in its own pass, so the + /// repetitions behind the numerator and the denominator are not paired: + /// there is no set of per-repetition ratios to take a median or a range + /// over. What can be said is that if one producer's cost lies in `[a, b]` + /// and N producers' in `[c, d]`, their ratio cannot fall outside + /// `[a / d, b / c]` -- so this is the widest the scaling could be, which is + /// conservative in the direction that matters. + /// + /// Carried because [`d-observations-not-verdicts`] obliges every published + /// figure to arrive with its dispersion, and a *derived* figure is exactly + /// where a bare point estimate is most likely to be over-read. Reporting + /// the ratio alone, while the rows beneath it carry ranges, would put the + /// least certain number on the page in the most confident dress. + /// + /// Pairing the repetitions would give a real distribution rather than a + /// bound, and that is a change to how the probe measures rather than to how + /// it reports -- see `M4.4`, which asks for candidates to be interleaved + /// with their controls. + /// + /// [`d-observations-not-verdicts`]: https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/DESIGN-NOTES.md#d-observations-not-verdicts + #[must_use] + pub fn scaling_bounds( + &self, + regime: &[Run], + shape: &str, + producers: usize, + ) -> Option<(f64, f64)> { + let one = self.find(regime, shape, 1)?; + let many = self.find(regime, shape, producers)?; + ratio_bounds(many, one) + } +} + +/// The interval a `numerator / denominator` cost ratio could occupy, given each +/// row's observed span. See [`Observation::scaling_bounds`] for why this is a +/// bound rather than a sample. +/// +/// The ratio is of *rates*, so it inverts the cost interval: a numerator that +/// was slow and a denominator that was fast give the smallest ratio. +/// +/// `None` when either span touches zero, which cannot happen for a real run and +/// is reported rather than divided by. +#[must_use] +pub fn ratio_bounds(numerator: Run, denominator: Run) -> Option<(f64, f64)> { + if numerator.fastest_nanos_per_op <= 0.0 + || numerator.slowest_nanos_per_op <= 0.0 + || denominator.fastest_nanos_per_op <= 0.0 + || denominator.slowest_nanos_per_op <= 0.0 + { + return None; + } + // Rate is inversely proportional to cost, so the widest rate ratio pairs + // the numerator's best cost against the denominator's worst, and vice versa. + let low = denominator.fastest_nanos_per_op / numerator.slowest_nanos_per_op; + let high = denominator.slowest_nanos_per_op / numerator.fastest_nanos_per_op; + Some((low, high)) +} + +/// Renders a ratio together with the interval it could occupy, or `--`. +/// +/// The bound is printed in square brackets to mark it as *not* a sampled range: +/// the row ranges above it are observed spans, this is arithmetic over two of +/// them. +#[must_use] +pub fn format_ratio_bounded(numerator: Option, denominator: Option) -> String { + match (numerator, denominator) { + (Some(numerator), Some(denominator)) if denominator.nanos_per_op > 0.0 => { + let point = numerator.nanos_per_op / denominator.nanos_per_op; + match ratio_bounds(denominator, numerator) { + Some((low, high)) => format!("{point:.2}x [{low:.2}-{high:.2}]"), + None => format!("{point:.2}x"), + } + } + _ => "--".to_owned(), + } +} + +/// Renders a scaling factor together with the interval it could occupy. +/// +/// See [`Observation::scaling_bounds`]: the bracketed interval is a bound over +/// two unpaired spans, not a distribution. +#[must_use] +pub fn format_scaling_bounded(point: Option, bounds: Option<(f64, f64)>) -> String { + match (point, bounds) { + (Some(point), _) if !point.is_finite() => "--".to_owned(), + (Some(point), Some((low, high))) if low.is_finite() && high.is_finite() => { + format!("{point:.2}x [{low:.2}-{high:.2}]") + } + (Some(point), _) => format!("{point:.2}x"), + (None, _) => "--".to_owned(), + } } /// Renders one regime's rows as the report's table body. @@ -422,13 +518,17 @@ fn median_run( mut timer: impl FnMut(usize) -> Repetition, ) -> Run { // One untimed pass first. Be exact about what this does and does not warm: - // every call to `timer` builds and drops its OWN queue, so this does not - // pre-touch the allocation any timed repetition will use. What it does warm - // is the process -- the allocator's size class, the OS page cache, the - // instruction cache, and the branch predictors -- which is why the first - // timed repetition is no longer an outlier. An earlier comment here claimed - // it faulted in "the" allocation, which is not true of an allocation made - // fresh each pass. Found by a review. + // for the queue timers, every call to `timer` builds and drops its OWN + // queue, so this does not pre-touch the allocation any timed repetition will + // use. The baseline timer allocates no queue at all -- one `AtomicU64` and a + // barrier -- so for that row there is no allocation to pre-touch either way. + // What the pass warms in both cases is the process: the allocator's size + // class, the OS page cache, the instruction cache, and the branch + // predictors, which is why the first timed repetition is no longer an + // outlier. An earlier comment here claimed it faulted in "the" allocation, + // which is not true of an allocation made fresh each pass, and a later one + // said every timer builds a queue, which is not true of the baseline. + // Both found by review. let _ = timer(producers); let mut results: Vec = (0..REPETITIONS).map(|_| timer(producers)).collect(); diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 3cbd23732..0160b827e 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -678,3 +678,159 @@ fn render_table_publishes_the_range_and_the_spread() { "the spread is missing from {line:?}" ); } + +/// Helper: a row with an explicit cost span, for the bound arithmetic. +fn run_spanning( + shape: &'static str, + producers: usize, + fastest: f64, + median: f64, + slowest: f64, +) -> Run { + Run { + shape, + producers, + nanos_per_op: median, + ops_per_second: if median > 0.0 { + 1_000_000_000.0 / median + } else { + 0.0 + }, + refusals: 0, + fastest_nanos_per_op: fastest, + slowest_nanos_per_op: slowest, + } +} + +/// The bound pairs each side's extreme against the other's opposite extreme, +/// because that is the widest the ratio could be. Rate ratio inverts cost, so +/// the smallest rate ratio is the numerator at its slowest against the +/// denominator at its fastest. +#[test] +fn ratio_bounds_pairs_opposing_extremes() { + let numerator = run_spanning(shapes::RESERVING_MPSC, 4, 8.0, 10.0, 12.0); + let denominator = run_spanning(shapes::SLOTWISE_MPSC, 4, 40.0, 50.0, 60.0); + let (low, high) = ratio_bounds(numerator, denominator).expect("both spans are positive"); + // rate ratio low = den.fastest / num.slowest = 40 / 12 + // rate ratio high = den.slowest / num.fastest = 60 / 8 + assert!((low - (40.0 / 12.0)).abs() < 1e-9, "low was {low}"); + assert!((high - (60.0 / 8.0)).abs() < 1e-9, "high was {high}"); +} + +/// The invariant that makes the bound meaningful: whatever point estimate the +/// medians produce must lie inside it. A bound that excluded its own point +/// estimate would be arithmetic nobody should trust. +#[test] +fn ratio_bounds_contain_the_point_estimate() { + let numerator = run_spanning(shapes::RESERVING_MPSC, 4, 8.0, 10.0, 12.0); + let denominator = run_spanning(shapes::SLOTWISE_MPSC, 4, 40.0, 50.0, 60.0); + let point = denominator.nanos_per_op / numerator.nanos_per_op; + let (low, high) = ratio_bounds(numerator, denominator).expect("both spans are positive"); + assert!( + low <= point && point <= high, + "the point estimate {point} falls outside its own bound [{low}, {high}]" + ); +} + +/// A row whose span touches zero cannot be divided by, the same case +/// `format_ratio` and `spread` already guard. +#[test] +fn ratio_bounds_of_a_zero_span_is_none() { + let real = run_spanning(shapes::RESERVING_MPSC, 4, 8.0, 10.0, 12.0); + let absent = run_spanning(shapes::SLOTWISE_MPSC, 4, 0.0, 0.0, 0.0); + assert!(ratio_bounds(real, absent).is_none()); + assert!(ratio_bounds(absent, real).is_none()); +} + +/// A configuration whose repetitions all agreed gives a bound of zero width, +/// which is what "this host held still" looks like for a derived figure. +#[test] +fn ratio_bounds_of_two_exact_samples_is_a_point() { + let numerator = run_spanning(shapes::RESERVING_MPSC, 4, 10.0, 10.0, 10.0); + let denominator = run_spanning(shapes::SLOTWISE_MPSC, 4, 50.0, 50.0, 50.0); + let (low, high) = ratio_bounds(numerator, denominator).expect("positive spans"); + assert!( + (low - 5.0).abs() < 1e-9 && (high - 5.0).abs() < 1e-9, + "[{low},{high}]" + ); +} + +#[test] +fn format_ratio_bounded_renders_the_point_and_its_interval() { + let numerator = run_spanning(shapes::RESERVING_MPSC, 4, 8.0, 10.0, 12.0); + let denominator = run_spanning(shapes::SLOTWISE_MPSC, 4, 40.0, 50.0, 60.0); + let rendered = format_ratio_bounded(Some(numerator), Some(denominator)); + assert!( + rendered.starts_with("0.20x ["), + "expected the point estimate first, got {rendered:?}" + ); + assert!( + rendered.contains('[') && rendered.contains(']'), + "the bound must be bracketed to mark it as not a sampled range: {rendered:?}" + ); +} + +#[test] +fn format_ratio_bounded_marks_a_missing_or_zero_row() { + let real = run_spanning(shapes::RESERVING_MPSC, 4, 8.0, 10.0, 12.0); + let zero = run_spanning(shapes::SLOTWISE_MPSC, 4, 0.0, 0.0, 0.0); + assert_eq!(format_ratio_bounded(None, Some(real)), "--"); + assert_eq!(format_ratio_bounded(Some(real), None), "--"); + assert_eq!(format_ratio_bounded(Some(real), Some(zero)), "--"); +} + +#[test] +fn format_scaling_bounded_renders_point_and_interval_or_the_marker() { + assert_eq!( + format_scaling_bounded(Some(2.0), Some((1.5, 2.5))), + "2.00x [1.50-2.50]" + ); + assert_eq!(format_scaling_bounded(None, Some((1.5, 2.5))), "--"); + assert_eq!( + format_scaling_bounded(Some(f64::NAN), Some((1.0, 2.0))), + "--" + ); + assert_eq!( + format_scaling_bounded(Some(f64::INFINITY), Some((1.0, 2.0))), + "--" + ); + // A point estimate with no computable bound still renders, unbracketed. + assert_eq!(format_scaling_bounded(Some(2.0), None), "2.00x"); +} + +#[test] +fn scaling_bounds_reads_the_one_and_many_producer_rows() { + let observation = Observation { + isolated: vec![ + run_spanning(shapes::RESERVING_MPSC, 1, 4.0, 5.0, 6.0), + run_spanning(shapes::RESERVING_MPSC, 8, 40.0, 50.0, 60.0), + ], + drained: Vec::new(), + available_parallelism: Some(8), + }; + let (low, high) = observation + .scaling_bounds(&observation.isolated, shapes::RESERVING_MPSC, 8) + .expect("both rows present with positive spans"); + let point = observation + .scaling(&observation.isolated, shapes::RESERVING_MPSC, 8) + .expect("both rows present"); + assert!( + low <= point && point <= high, + "scaling {point} outside its bound [{low}, {high}]" + ); +} + +#[test] +fn scaling_bounds_is_none_when_a_row_is_missing() { + let observation = Observation { + isolated: vec![run_spanning(shapes::RESERVING_MPSC, 8, 40.0, 50.0, 60.0)], + drained: Vec::new(), + available_parallelism: Some(8), + }; + assert!( + observation + .scaling_bounds(&observation.isolated, shapes::RESERVING_MPSC, 8) + .is_none(), + "the one-producer row is absent, so no bound exists" + ); +} From 8950987c2a4540bfe4609a4c0d0b0c3f7559a46c Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 20:30:21 -0700 Subject: [PATCH 051/139] docs: count the tabular/prose split instead of estimating it The note said 'roughly half the restatements are tabular and mechanically checkable' without stating a denominator or having measured it. Counted: of the 42 occurrences of those five figures across the crate's .rs, .md and .toml files, 19 sit in table rows and 23 in prose. So the estimate was right and the method was not -- a proportion used to scope a proposed remedy should be measured, especially in a note whose subject is claims that outrun their evidence. The note's own table had also drifted: 2^56 gained an occurrence when the horizon qualifier was added to Perpetual's rustdoc. Updated, and labelled as a measurement with a date-of rather than a standing fact -- the table demonstrating its own subject. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index f28c077ac..d77e985ab 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1883,10 +1883,13 @@ produced most of the review findings, single facts are restated like this: |---|---|---| | `255` (the `Perpetual` reservation-count ceiling) | 19 | 5 | | `37 seconds` (the `Balanced` recurrence horizon) | 8 | 4 | -| `2^56` (the `Perpetual` position span) | 6 | 3 | +| `2^56` (the `Perpetual` position span) | 7 | 4 | | `4,294,967,295` (the `Balanced` field ceiling) | 5 | 3 | | `about 20 years` | 3 | 3 | +*(Counts are as of the measurement, and they move: `2^56` gained an occurrence when the horizon +qualifier was added to `Perpetual`'s rustdoc, which is the table demonstrating its own subject.)* + **All of these are restated by hand with nothing checking them, and most are derivable from `ClaimLayout`'s associated constants.** The two time rows are not: `37 seconds` and `about 20 years` follow from a field width *and* an assumed sustained push rate, so a constants-versus-table check @@ -1960,11 +1963,13 @@ the two errors this note originally named -- the `2^31`/`2^30` target-dependent `MAX_RESERVED`-as-capacity conflation -- it would **not** have caught at all: both are prose assertions in the surrounding text, not cells in any table. -That bound is the useful part rather than a caveat on it. Roughly half the restatements measured -above are tabular and mechanically checkable; the other half are prose claims *about* those -constants, and catching those needs something that reads assertions rather than rows. A remedy that -covers the first half is still worth having, and claiming it covers both is how a partial instrument -comes to be trusted as a complete one. +That bound is the useful part rather than a caveat on it. Counted rather than estimated, and stating +the denominator because an earlier version of this sentence did not: of the **42** occurrences of +those five figures across the crate's `.rs`, `.md` and `.toml` files, **19 sit in table rows** and +**23 in prose**. So a constants-versus-table check reaches a little under half of them, and the rest +are prose claims *about* those constants, which need something that reads assertions rather than +rows. A remedy that covers the tabular half is still worth having, and claiming it covers both is how +a partial instrument comes to be trusted as a complete one. **No work is scheduled by this note.** It was written to inform a decision that has not been taken, and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule From e75bc7ea512e3992a5df2beb0dcdbc69c7cf71e2 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 20:53:37 -0700 Subject: [PATCH 052/139] docs: prose carries the claim, an artifact carries the number Records a principle rather than scheduling a change, because the mechanism is not decided. Measured data pasted into prose becomes a copy somebody must keep true by hand, in every place it was pasted, forever. Markdown has no include and rustdoc has no data include, so pasting is the path of least resistance -- and it is where this repository's documentation defects overwhelmingly come from. On PR 90's review history, almost none of the measurement findings were WRONG MEASUREMENTS. They were transcriptions that drifted: a table disagreeing with its own copy one file away, an attribution naming a capture the figures no longer came from, one recurrence horizon left unqualified across seven sites in three wordings, a withdrawn magnitude surviving in two public rustdocs. The instance that earned the rule was in this instructions file. It claimed "in both cases roughly 60%" about two figures given four words earlier in the same sentence, one of which was 57 of 61 -- 93%. The data was adjacent and the prose summary of it was false, because prose is not checkable and nobody checks it. That proportion is now deleted; the counts it restated were already there. So: a claim belongs in prose, because it contains no digits and cannot drift from the data -- it can only be wrong about it, which a reader can see. A number belongs in one place, with its provenance travelling alongside rather than in a hand-maintained attribution table. And a proportion over data we already showed is not a finding at all, it is a hand-computed copy of one; the counts are the finding, and a reader who wants a ratio can take one against a denominator they chose at a moment they know. Removed the proportions from the root design note accordingly -- "roughly half of each row", "the large majority", "a little under half", and the crate that "produced most of the review findings". Each was arithmetic over a table sitting directly above it. This repository already contains the better pattern and did not apply it here: mutation-sweeps// is a dated, committed capture directory, while windows-platform-probes, which produces the most-cited numbers in the workspace, commits no capture at all. Noted in the design note as the asymmetry it is. No work is scheduled. The reader-experience trade is real -- a figure behind a link is a figure most readers will not look at -- and it is undecided. Per "design notes are not a work queue", the absence of a checklist item is deliberate and stated in the note. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 32 +++++++++++- DESIGN-NOTES.md | 88 ++++++++++++++++++++++++++------- 2 files changed, 99 insertions(+), 21 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4e07744d1..d6792d13d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -492,8 +492,7 @@ the same time, so both halves of the evidence disappear together. This is not a small correction. Measured on two runs here: a `windows-topology-sys` sweep reported 61 survivors of which **57 were in `#[cfg(feature = "serde")]` code**, and a `windows-file-watcher` sweep reported 247 of which **147 were in `scenario-tool` and -`test-util` modules**. In both cases roughly 60% of the "gaps" were artifacts of the -invocation. So: +`test-util` modules**. In both cases the gated code dominated the survivor list. So: ``` cargo mutants -p --all-features @@ -1271,6 +1270,35 @@ Two corollaries that have each already cost a review round: written while the old reading was current — generators, test doubles, examples — because those encode the reading rather than citing it. +### 4. Prose carries the claim; a number belongs in an artifact + +Measured data pasted into prose becomes a copy somebody must keep true by hand, in every place it +was pasted, forever. Markdown has no include and rustdoc has no data include, so pasting is the path +of least resistance — and it is where this repository's documentation defects overwhelmingly come +from. Measured on one pull request's review history: almost none of its measurement findings were +*wrong measurements*; they were transcriptions that drifted — a table disagreeing with its own copy +one file away, an attribution naming a superseded capture, one horizon left unqualified across seven +sites in three wordings. + +- **Write the claim, not the digits, wherever the digits are not the point.** "Measured faster under + contention, and the spread is wide enough that the ordering is a flag rather than a finding" cannot + drift from the data, because it restates none of it. +- **When a figure must appear, it has exactly one home.** Prefer a committed capture the prose links + to (`mutation-sweeps//` is this repository's existing example) over the same figure typed + into two documents. Provenance — host, commit, date — travels with the data rather than in a + hand-maintained table beside it. +- **Never restate a proportion over data you already showed.** A ratio over counts in the same + document is not a finding; it is a hand-computed copy of one, checked by nobody and stale the + moment any input moves. The counts are the finding. This rule was earned: an instructions file in + this repository claimed "in both cases roughly 60%" about two figures given four words earlier, + one of which was 57 of 61 — 93%. +- **The same applies to incidental tallies** — test counts, file counts, line counts. If the number + is not itself the finding, leave it out; "the gate is green" says what "308 lib tests" pretends to. + +**This is the data-side twin of rule 1.** Rule 1 says define a fact once in code and have everything +ask. This says the same of measurements: hold the number once, and have prose point rather than +paraphrase. + ## REVIEW FEEDBACK — answer it where it was raised, not only in the commit **A review round is not finished when the code changes. It is finished when the reviewer has diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index d77e985ab..56870f5e3 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1876,8 +1876,7 @@ surface. Across the workspace, prose runs at **0.84 lines per line of code** -- about 86,000 lines of prose (50,700 Rust comment lines, 35,200 markdown) against 101,700 lines of code. -That number turns out to be the wrong one to watch. In `windows-waitable-queues`, the crate that -produced most of the review findings, single facts are restated like this: +That number turns out to be the wrong one to watch. In `windows-waitable-queues`, single facts are restated like this: | fact | restatements | files | |---|---|---| @@ -1890,16 +1889,14 @@ produced most of the review findings, single facts are restated like this: *(Counts are as of the measurement, and they move: `2^56` gained an occurrence when the horizon qualifier was added to `Perpetual`'s rustdoc, which is the table demonstrating its own subject.)* -**All of these are restated by hand with nothing checking them, and most are derivable from -`ClaimLayout`'s associated constants.** The two time rows are not: `37 seconds` and `about 20 years` +**All of these are restated by hand with nothing checking them.** Three of the rows -- the ceiling, +the span and the field ceiling -- follow from `ClaimLayout`'s associated constants. The two time rows follow from a field width *and* an assumed sustained push rate, so a constants-versus-table check -would validate the first three rows outright and the time rows only once the rate is also pinned -somewhere single. That distinction matters because it bounds what the cheapest remedy below can -actually do -- an earlier version of this paragraph said every row was derivable from the constants, -which overstated it, in a note about overstatement. The error surface is proportional to that column, -not to -total prose volume. Halving the prose uniformly would leave roughly half of each row and fix -nothing structural. +would validate the first three outright and the time rows only once the rate is pinned somewhere +single. That distinction bounds what the cheapest remedy below can do -- an earlier version of this +paragraph said every row was derivable from the constants, which overstated it, in a note about +overstatement. The error surface is proportional to that column, not to +total prose volume: a uniform cut to the prose leaves every row still restated, just in fewer words. ### Which errors this predicts, and which it does not @@ -1907,7 +1904,8 @@ Sorting PR #90's findings across all rounds by class: - **Restated derivable facts** -- the `2^31`/`2^30` target-dependent capacity, `MAX_RESERVED` conflated with capacity, "`Wide` removes it" for a bound that is finite, stale recurrence tables, - a test count that matched no crate. **The large majority.** + a test count that matched no crate, the same horizon left unqualified across seven sites, a + withdrawn magnitude surviving in two public rustdocs. - **Structural** -- an unmarked supersedence row in a decision index, an orphaned milestone reference. A linter's job, not a specification's. - **Evidence overclaiming** -- a noise floor computed from two runs, a refusal-count argument that @@ -1947,6 +1945,53 @@ level up: define the protocol once in a form that can be checked, and let every This is the real connection between the two ideas, and it is why they belong in the same conversation despite fixing different things. +### Prose carries the claim; an artifact carries the number + +The sharper question, asked after several rounds of the above: **why is measured data living in +prose at all?** + +There is no principled reason. It is an accident of what is easy. Markdown has no include and +rustdoc has no data include, so the only way to put a figure in front of a reader is to paste it -- +and a pasted figure is a copy somebody must keep true by hand, in every place they pasted it, +forever. + +The cost is measurable in this PR's own review history. Almost none of its measurement-related +findings were *wrong measurements*. They were **transcription failures**: the same table in the +README and the crate rustdoc disagreeing because one was retaken; an attribution naming a capture +the figures no longer came from; one recurrence horizon left unqualified across seven sites in three +wordings; a withdrawn magnitude surviving in two public rustdocs. The most instructive was a +proportion that restated two counts **given four words earlier in the same sentence** and got one of +them wrong -- "in both cases roughly 60%", against 57 of 61, which is 93%. The data was adjacent and +the summary of it was false, because prose is not checkable and nobody checks it. + +**This repository already contains the better pattern and did not apply it here.** +`mutation-sweeps/2026-09-02/` is a dated, committed capture directory: data as an artifact, cited +rather than retyped. `windows-platform-probes`, which produces the most-cited numbers in the +workspace, commits no capture at all -- every figure it has ever published reached its document by +hand. + +So the principle, which holds regardless of which mechanism is eventually chosen: + +- **A claim belongs in prose.** "`reserving_mpsc` measured faster than `slotwise_mpsc` under + contention, and the spread is wide enough that the ordering is a flag rather than a finding" is a + claim. It contains no digits, so it cannot drift from the data -- it can only be wrong about it, + which a reader can see. +- **A number belongs in an artifact.** `15.3`, `246.9`, `fecd352`, a count of occurrences: one copy, + with its provenance travelling *with* it rather than in a hand-maintained attribution table + beside it. +- **A proportion over data we hold is not a finding, it is a restatement of one.** Computed by hand, + checked by nobody, and stale the moment any input moves. The counts are the finding. A reader who + wants a ratio can take one, against a denominator they chose and at a moment they know. + +If this were adopted, the "which restatements are mechanically checkable" question earlier in this +note **dissolves** rather than being answered: all of them, because none would be restated. + +**The mechanism is undecided and no work is scheduled here.** The reader-experience trade is real -- +a figure behind a link is a figure most readers will not look at -- and it has not been settled. +Recorded as a principle so the next person choosing where to paste a number has the argument in front +of them, not as a queued change. Per "design notes are not a work queue", the absence of a checklist +item is deliberate. + ### The cheapest available move, recorded but not scheduled [README.md](crates/windows-waitable-queues/README.md) is already a build input for @@ -1963,13 +2008,18 @@ the two errors this note originally named -- the `2^31`/`2^30` target-dependent `MAX_RESERVED`-as-capacity conflation -- it would **not** have caught at all: both are prose assertions in the surrounding text, not cells in any table. -That bound is the useful part rather than a caveat on it. Counted rather than estimated, and stating -the denominator because an earlier version of this sentence did not: of the **42** occurrences of -those five figures across the crate's `.rs`, `.md` and `.toml` files, **19 sit in table rows** and -**23 in prose**. So a constants-versus-table check reaches a little under half of them, and the rest -are prose claims *about* those constants, which need something that reads assertions rather than -rows. A remedy that covers the tabular half is still worth having, and claiming it covers both is how -a partial instrument comes to be trusted as a complete one. +That bound is the useful part rather than a caveat on it. A constants-versus-table check reaches the +occurrences that sit in table rows and none of the ones in prose, and both populations are +substantial -- which is the shape of the result, and a reason to build the check rather than not to. +The prose occurrences need something that reads assertions rather than rows. A remedy that covers the +tabular ones is worth having; claiming it covers both is how a partial instrument comes to be trusted +as a complete one. + +*(An earlier version of this paragraph put a proportion here. It is gone deliberately: a ratio over +the counts above is a restatement of them, computed by hand and checked by nobody, and it drifts the +moment any file is edited -- which is the defect this whole note is about. The counts are the +finding. Anyone who needs a proportion can take one, against a denominator they chose and at a moment +they know.)* **No work is scheduled by this note.** It was written to inform a decision that has not been taken, and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule From a8d4c7f4cb46ef4469bda5adc8ff8a6c33a88354 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 21:05:54 -0700 Subject: [PATCH 053/139] fix(platform-probes): ratio_bounds returned the inverse of what it documented `ratio_bounds` said it gave the interval for a `numerator / denominator` cost ratio and returned the rate ratio instead -- `denominator / numerator`. Both internal callers compensated by passing their rows in the opposite order, so every rendered figure was correct and the defect was invisible from the output. It was visible to anyone calling the function, which is the point: a public contract that is only correct if you read the implementation is not a contract. Found by review. The arithmetic now matches the documentation -- `numerator.fastest / denominator.slowest` through `numerator.slowest / denominator.fastest` -- and each caller passes its rows in the order matching what it wants. `format_ratio_bounded` wants a cost ratio and passes them straight; `scaling_bounds` wants a rate ratio, which is the cost ratio of one-producer over many, and says so at the call. **No published figure changes.** The two swaps cancelled, so the report renders identically before and after; verified by rebuilding and reading it. The drained table's first row is now checkable by hand from its own columns -- 25.9 over 13.0 is 1.99x -- which it was before and nobody could tell. Also fixes the drained table's header, which abbreviated one unit to `ns/pu` while every other table and test in the crate says `ns/op`, and which had been truncated to fit a column the bounded ratios then outgrew. The table now has a two-line header naming the unit for each column and marking the ratio columns as carrying a bound. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 11 ++++-- .../src/queue_contention.rs | 35 +++++++++++++------ .../src/queue_contention/tests.rs | 13 +++---- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 7a19bda00..8be3b286d 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -185,8 +185,13 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " {:<18} {:>14} {:>14} {:>10} {:>14} {:>16}", - "producers", "slotwise ns/pu", "reserving", "ratio", "permit", "permit/reserving" + " {:<10} {:>12} {:>12} {:>22} {:>12} {:>22}", + "producers", "slotwise", "reserving", "reserving/slotwise", "permit", "permit/reserving" + ); + let _ = writeln!( + out, + " {:<10} {:>12} {:>12} {:>22} {:>12} {:>22}", + "", "ns/op", "ns/op", "ratio [bound]", "ns/op", "ratio [bound]" ); for &producers in PRODUCER_COUNTS { let plain = observation.find(&observation.drained, shapes::SLOTWISE_MPSC, producers); @@ -199,7 +204,7 @@ fn render(out: &mut dyn std::fmt::Write) { let permit_ratio = format_ratio_bounded(permit, reserving); let _ = writeln!( out, - " {producers:<18} {:>14} {:>14} {:>10} {:>14} {:>16}", + " {producers:<10} {:>12} {:>12} {:>22} {:>12} {:>22}", format_nanos(plain), format_nanos(reserving), ratio, diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 3cdfccacf..0ac2ddef1 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -295,16 +295,29 @@ impl Observation { ) -> Option<(f64, f64)> { let one = self.find(regime, shape, 1)?; let many = self.find(regime, shape, producers)?; - ratio_bounds(many, one) + // Scaling is a RATE ratio -- many over one -- which is the COST ratio + // one over many, so the rows go in that order. + ratio_bounds(one, many) } } -/// The interval a `numerator / denominator` cost ratio could occupy, given each -/// row's observed span. See [`Observation::scaling_bounds`] for why this is a -/// bound rather than a sample. +/// The interval the cost ratio `numerator / denominator` could occupy, given +/// each row's observed span. /// -/// The ratio is of *rates*, so it inverts the cost interval: a numerator that -/// was slow and a denominator that was fast give the smallest ratio. +/// Both arguments are rows whose costs are nanoseconds per operation, and the +/// result is in those same terms: `0.20x` means the numerator cost a fifth of +/// what the denominator cost. A caller wanting a *rate* ratio -- "how much +/// faster" -- passes the two rows the other way round, which is what +/// [`Observation::scaling_bounds`] does. +/// +/// See [`Observation::scaling_bounds`] for why this is a bound rather than a +/// sample. +/// +/// **An earlier version of this function documented a cost ratio and returned +/// the inverse**, leaving both internal callers to compensate by swapping their +/// arguments. That works until somebody calls it directly, which is the defect a +/// review caught: a public contract that is only correct if you read the +/// implementation is not a contract. /// /// `None` when either span touches zero, which cannot happen for a real run and /// is reported rather than divided by. @@ -317,10 +330,10 @@ pub fn ratio_bounds(numerator: Run, denominator: Run) -> Option<(f64, f64)> { { return None; } - // Rate is inversely proportional to cost, so the widest rate ratio pairs - // the numerator's best cost against the denominator's worst, and vice versa. - let low = denominator.fastest_nanos_per_op / numerator.slowest_nanos_per_op; - let high = denominator.slowest_nanos_per_op / numerator.fastest_nanos_per_op; + // Widest is the numerator at its worst over the denominator at its best; + // narrowest is the reverse. + let low = numerator.fastest_nanos_per_op / denominator.slowest_nanos_per_op; + let high = numerator.slowest_nanos_per_op / denominator.fastest_nanos_per_op; Some((low, high)) } @@ -334,7 +347,7 @@ pub fn format_ratio_bounded(numerator: Option, denominator: Option) -> match (numerator, denominator) { (Some(numerator), Some(denominator)) if denominator.nanos_per_op > 0.0 => { let point = numerator.nanos_per_op / denominator.nanos_per_op; - match ratio_bounds(denominator, numerator) { + match ratio_bounds(numerator, denominator) { Some((low, high)) => format!("{point:.2}x [{low:.2}-{high:.2}]"), None => format!("{point:.2}x"), } diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 0160b827e..50c75068c 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -711,10 +711,10 @@ fn ratio_bounds_pairs_opposing_extremes() { let numerator = run_spanning(shapes::RESERVING_MPSC, 4, 8.0, 10.0, 12.0); let denominator = run_spanning(shapes::SLOTWISE_MPSC, 4, 40.0, 50.0, 60.0); let (low, high) = ratio_bounds(numerator, denominator).expect("both spans are positive"); - // rate ratio low = den.fastest / num.slowest = 40 / 12 - // rate ratio high = den.slowest / num.fastest = 60 / 8 - assert!((low - (40.0 / 12.0)).abs() < 1e-9, "low was {low}"); - assert!((high - (60.0 / 8.0)).abs() < 1e-9, "high was {high}"); + // cost ratio low = num.fastest / den.slowest = 8 / 60 + // cost ratio high = num.slowest / den.fastest = 12 / 40 + assert!((low - (8.0 / 60.0)).abs() < 1e-9, "low was {low}"); + assert!((high - (12.0 / 40.0)).abs() < 1e-9, "high was {high}"); } /// The invariant that makes the bound meaningful: whatever point estimate the @@ -724,7 +724,7 @@ fn ratio_bounds_pairs_opposing_extremes() { fn ratio_bounds_contain_the_point_estimate() { let numerator = run_spanning(shapes::RESERVING_MPSC, 4, 8.0, 10.0, 12.0); let denominator = run_spanning(shapes::SLOTWISE_MPSC, 4, 40.0, 50.0, 60.0); - let point = denominator.nanos_per_op / numerator.nanos_per_op; + let point = numerator.nanos_per_op / denominator.nanos_per_op; let (low, high) = ratio_bounds(numerator, denominator).expect("both spans are positive"); assert!( low <= point && point <= high, @@ -749,8 +749,9 @@ fn ratio_bounds_of_two_exact_samples_is_a_point() { let numerator = run_spanning(shapes::RESERVING_MPSC, 4, 10.0, 10.0, 10.0); let denominator = run_spanning(shapes::SLOTWISE_MPSC, 4, 50.0, 50.0, 50.0); let (low, high) = ratio_bounds(numerator, denominator).expect("positive spans"); + // Cost ratio: 10 over 50. assert!( - (low - 5.0).abs() < 1e-9 && (high - 5.0).abs() < 1e-9, + (low - 0.2).abs() < 1e-9 && (high - 0.2).abs() < 1e-9, "[{low},{high}]" ); } From 3761c63e0249d4b83c22cd7da05d102099de2d60 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 21:06:17 -0700 Subject: [PATCH 054/139] docs: the field binds only once the queue is at least that large Six sites said the reservation-count field is what limits `Enduring` and `Perpetual`, full stop. It is not. The achievable count is the lesser of the queue's capacity and the field ceiling, so a `Perpetual` queue of capacity 64 admits 64 reservations rather than 255 -- and the README's own worked example uses exactly that capacity, so it stood as a counterexample to the sentence three paragraphs above it. The crate had this right where it was tested and wrong where it was described. An earlier sabotage on this branch already demonstrated it: the same test that fills Perpetual to 255 at capacity 1024 stops at 128 when the capacity is 128. The evidence was in the branch and the prose contradicted it. Corrected in the README, the crate rustdoc, `ClaimLayout`'s table note, the `Balanced` comparison, and both the `Enduring` and `Perpetual` type docs, which said "Holds 65,535" and "Holds 255" as though every instance could. Three corrections in the root design note, all to claims about its own analysis rather than about the code: - "validate the first three rows" named an ordinal position; the table's second row is a time row, so the sentence contradicted the one before it. Named the three rows instead of counting them. - "converting 19 hand-written 255s into checked derivations" overstated the proposed check: the 19 spans all five figures and includes prose occurrences a table parser never sees. Now says it reaches the tabular occurrences and only those. - the note pointed at `M30` in the root checklist, which does not exist on this branch -- it is queued on another. Removing the branch-status qualifier last round for durability turned a stale reference into a broken one. The note now says a survey is queued separately without naming an ID it cannot resolve. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 17 +++++++---------- crates/windows-waitable-queues/README.md | 2 +- crates/windows-waitable-queues/src/lib.rs | 5 ++++- .../src/reserving_mpsc.rs | 19 +++++++++++++------ 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 56870f5e3..e38e31cce 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1892,7 +1892,7 @@ qualifier was added to `Perpetual`'s rustdoc, which is the table demonstrating i **All of these are restated by hand with nothing checking them.** Three of the rows -- the ceiling, the span and the field ceiling -- follow from `ClaimLayout`'s associated constants. The two time rows follow from a field width *and* an assumed sustained push rate, so a constants-versus-table check -would validate the first three outright and the time rows only once the rate is pinned somewhere +would validate those three outright and the time rows only once the rate is pinned somewhere single. That distinction bounds what the cheapest remedy below can do -- an earlier version of this paragraph said every row was derivable from the constants, which overstated it, in a note about overstatement. The error surface is proportional to that column, not to @@ -1997,8 +1997,9 @@ item is deliberate. [README.md](crates/windows-waitable-queues/README.md) is already a build input for `windows-waitable-queues` (`#[doc = include_str!]` in [lib.rs](crates/windows-waitable-queues/src/lib.rs)), so a test can parse the published layout -tables and assert every row against `ClaimLayout`'s constants -- converting 19 hand-written `255`s -into one definition and N checked derivations, with no generator and no new tooling. +tables and assert every row against `ClaimLayout`'s constants -- turning the occurrences that sit in +table rows into checked derivations of one definition, with no generator and no new tooling. It +reaches only those; the occurrences in prose are untouched by it. **Be precise about what that would and would not catch, because this paragraph has now overstated it twice.** The layout table's columns are the layout name, the reservation-count field ceiling, the @@ -2023,10 +2024,6 @@ they know.)* **No work is scheduled by this note.** It was written to inform a decision that has not been taken, and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule -rather than an oversight. The formal-methods survey it bears on is `M30` in the root -[CHECKLIST.md](CHECKLIST.md). If the table-versus-constants test or a prose-reduction pass is -adopted, each needs its own item at that time. - -*(`M30` was authored as `M23` and renumbered when that ID was found to collide with an archived -milestone. Recorded because the old number appears in the commit history and in PR #92's earlier -discussion, not because either number is in doubt now.)* +rather than an oversight. A formal-methods survey is queued separately in the root +[CHECKLIST.md](CHECKLIST.md) and bears on the same question. If the table-versus-constants test or a +prose-reduction pass is adopted, each needs its own item at that time. diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 2fcc786b3..679a52e2f 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -145,7 +145,7 @@ admission is also bounded by capacity -- `reserve` refuses once the ring has no room beyond the reservations already outstanding -- so the achievable count is the lesser of the two. For `Balanced` the capacity bound binds first, since that layout accepts at most 2^31 slots on a 64-bit target, and 2^30 on a 32-bit -one. For the others the field binds on either. +one. For the others the field is the smaller number only once the queue is at least that large: a `Perpetual` queue of capacity 64 can hold 64 reservations, not 255. The achievable count is always the lesser of the two. ```rust use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 3d2bc3bb6..9adbde349 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -113,7 +113,10 @@ //! reservations: admission is also bounded by capacity, so the achievable count //! is the lesser of the two. For `Balanced` the capacity bound binds first -- //! that layout accepts at most 2^31 slots on a 64-bit target, and 2^30 on a -//! 32-bit one. For the others the field binds on either. +//! 32-bit one. For the others the field is the smaller number only once +//! the queue is at least that large: a `Perpetual` queue of capacity 64 can +//! hold 64 reservations, not 255. The achievable count is always the lesser +//! of the two. //! //! ``` //! use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index bbcabe160..b2b674547 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -235,8 +235,10 @@ use crate::options::Options; /// bound is the binding one: this layout accepts at most 2^31 slots on a 64-bit /// target and 2^30 on a 32-bit one, so no more than that many reservations can /// be outstanding whatever the field could hold. For -/// [`Enduring`] and [`Perpetual`] the field binds first, and the column is the -/// real limit. +/// [`Enduring`] and [`Perpetual`] the field is the smaller of the two only once +/// the queue is at least that large -- a `Perpetual` queue of capacity 64 admits +/// 64 reservations, not 255. The achievable count is always the lesser of +/// capacity and field ceiling; this column is the field half of that pair. /// /// At this crate's disclosed sustained rate of about 116 million pushes per /// second, those recurrences are roughly **37 seconds**, **28 days**, and @@ -556,8 +558,9 @@ impl ClaimWord for u128 { /// beyond any use this crate has seen -- and beyond what its own capacity /// permits -- while the exposure is what pays for it. /// [`Enduring`] and [`Perpetual`] spend that field the other way -- -/// [`Enduring`] holds 65,535 outstanding reservations, [`Perpetual`] 255, both -/// reachable because capacity does not bind there -- +/// [`Enduring`] holds up to 65,535 outstanding reservations, [`Perpetual`] up to +/// 255 -- each reachable only when the queue's capacity is at least that +/// large -- /// and `Wide` moves it to 2^64 pushes rather than to a horizon in years. (`Wide` exists /// only under the `dwcas` feature, so this names it without linking: an /// intra-doc link here would not resolve in a default-feature rustdoc build.) @@ -572,7 +575,9 @@ impl ClaimLayout for Balanced { /// A deeper position: 16 bits of reservations, 48 of position. /// -/// Holds 65,535 outstanding reservations and recurs after 2^48 pushes -- about +/// Its field holds at most 65,535 outstanding reservations -- reachable only +/// when capacity is at least that large, since the achievable count is the +/// lesser of the two -- and its position recurs after 2^48 pushes, about /// **28 days** at the pre-correction planning rate; see [`ClaimLayout`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Enduring; @@ -585,7 +590,9 @@ impl ClaimLayout for Enduring { /// The deepest position: 8 bits of reservations, 56 of position. /// -/// Holds 255 outstanding reservations and recurs after 2^56 pushes -- about +/// Its field holds at most 255 outstanding reservations -- reachable only when +/// capacity is at least that large, since the achievable count is the lesser of +/// the two -- and its position recurs after 2^56 pushes, about /// **20 years** at the pre-correction planning rate ([`ClaimLayout`]), which puts the recurrence /// beyond any real deployment rather than merely far away. /// From ce21ff3dba13d06220122959514774e2fe5cb776 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 21:13:41 -0700 Subject: [PATCH 055/139] docs: the section arguing against embedded figures was embedded figures This PR adds the rule that a number belongs in one place and prose should carry the claim. The section stating it opened with a prose-to-code ratio, three workspace line counts and a five-row census table, all hand-maintained and none reproducible. It was a counterexample to itself, which a review said plainly. It had also already failed twice. The table drifted within days when a rustdoc qualifier added an occurrence -- recorded in a parenthetical rather than fixed -- and re-running the census while making this change shows the ceiling at 22 occurrences where the table says 19, because corrections landed in this same session. A census of restatements had become a restatement needing maintenance. Now qualitative, with the command that computes the counts. A reader gets current numbers instead of a snapshot of someone else's, which is what 'let findings be findings computed directly' means when applied to this section rather than only recommended by it. The command was run before committing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 65 ++++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index e38e31cce..8aefcf4e3 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1871,32 +1871,47 @@ which must be inexact to serve human readers, is being asked to carry a specific bear, and whether some formal specification plus substantially less prose would shrink the error surface. -### The measurement - -Across the workspace, prose runs at **0.84 lines per line of code** -- about 86,000 lines of prose -(50,700 Rust comment lines, 35,200 markdown) against 101,700 lines of code. - -That number turns out to be the wrong one to watch. In `windows-waitable-queues`, single facts are restated like this: +### The measurement, and why it is not written down here + +Prose in this workspace runs at somewhat less than a line per line of code, counting Rust comment +lines and markdown together. That ratio turns out to be the wrong thing to watch. + +The thing to watch is that in `windows-waitable-queues`, a handful of single facts -- `Perpetual`'s +reservation-count ceiling, `Balanced`'s recurrence horizon, `Perpetual`'s position span, `Balanced`'s +field ceiling -- are each restated many times across several files, by hand, with nothing checking +any of them. The ceiling is the worst: it appears in five separate files. + +**The exact counts are deliberately not recorded here.** An earlier version of this section carried +them as a table, and the table drifted within days: one row gained an occurrence when a qualifier was +added to a rustdoc elsewhere in this same branch, so the census of restatements became a restatement +that needed maintaining. That is the section's own subject, demonstrated on the section. + +Anyone who wants current numbers can compute them, which is the point of the principle below -- the +counts are a finding, and a finding should be computed rather than quoted: + +```powershell +# Occurrences of a figure across the crate, and how many files carry it. +$files = git ls-files 'crates/windows-waitable-queues/*' | + Where-Object { $_ -match '\.(rs|md|toml)$' } +foreach ($pattern in '\b255\b', '37 seconds', '2\^56', '4,294,967,295', 'about 20 years') { + $hits = 0; $carrying = 0 + foreach ($file in $files) { + $n = ([regex]::Matches([System.IO.File]::ReadAllText($file), $pattern)).Count + if ($n) { $hits += $n; $carrying++ } + } + "{0,-16} {1,3} occurrences across {2} files" -f $pattern, $hits, $carrying +} +``` -| fact | restatements | files | -|---|---|---| -| `255` (the `Perpetual` reservation-count ceiling) | 19 | 5 | -| `37 seconds` (the `Balanced` recurrence horizon) | 8 | 4 | -| `2^56` (the `Perpetual` position span) | 7 | 4 | -| `4,294,967,295` (the `Balanced` field ceiling) | 5 | 3 | -| `about 20 years` | 3 | 3 | - -*(Counts are as of the measurement, and they move: `2^56` gained an occurrence when the horizon -qualifier was added to `Perpetual`'s rustdoc, which is the table demonstrating its own subject.)* - -**All of these are restated by hand with nothing checking them.** Three of the rows -- the ceiling, -the span and the field ceiling -- follow from `ClaimLayout`'s associated constants. The two time rows -follow from a field width *and* an assumed sustained push rate, so a constants-versus-table check -would validate those three outright and the time rows only once the rate is pinned somewhere -single. That distinction bounds what the cheapest remedy below can do -- an earlier version of this -paragraph said every row was derivable from the constants, which overstated it, in a note about -overstatement. The error surface is proportional to that column, not to -total prose volume: a uniform cut to the prose leaves every row still restated, just in fewer words. +**All of these are restated by hand with nothing checking them.** Three of those facts -- the +ceiling, the span and the field ceiling -- follow from `ClaimLayout`'s associated constants. The two +time figures follow from a field width *and* an assumed sustained push rate, so a +constants-versus-table check would validate those three outright and the time figures only once the +rate is pinned somewhere single. That distinction bounds what the cheapest remedy below can do -- an +earlier version of this paragraph said every one was derivable from the constants, which overstated +it, in a note about overstatement. The error surface is proportional to how often a fact is restated, +not to total prose volume: a uniform cut to the prose leaves every restatement in place, just in +fewer words. ### Which errors this predicts, and which it does not From 893a279f22c44a53f0c0d4c96aa320cbd1a8dccb Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 21:30:47 -0700 Subject: [PATCH 056/139] docs(waitable-queues): 2^64 is about 5,000 years, not "unreachable" Seven sites called `Wide`'s recurrence unreachable, or said no deployment reaches it -- two as the table cell "unreachable", five as prose. The column those cells sit in is explicitly arithmetic over a finite rate, and the rows beside them read 37 seconds, 28 days and 20 years. Computed at the same rate the table documents, 2^64 pushes is about 5,000 years. So the crate published an absolute claim in a column of finite arithmetic, three cells away from the arithmetic that contradicts it. All seven now give the figure, and say it is a longer horizon rather than the absence of one, moving with the caller's rate like every other entry in that column. Verified by computing all four rows: 2^32 gives 37 seconds and 2^56 gives 19.7 years, which match what was already published, so the rate model is the same one. This is the table-cell form of a claim already withdrawn twice in this branch as prose -- "Wide removes it" went through the same correction rounds ago. A cell is a restatement like any other and the sweeps that fixed the sentences did not reach it, because a one-word cell does not read like a claim. The module docs also still said `slotwise_mpsc` is "for a caller who wants the cheapest possible push". That asserts a cost ordering this crate does not establish and that its own end-to-end measurement did not find -- the same measurement whose causal reading was withdrawn earlier in this branch. The two shapes are now distinguished by what each offers rather than by which is quicker, with the withdrawn phrasing recorded rather than silently dropped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/README.md | 7 ++++--- crates/windows-waitable-queues/src/lib.rs | 8 ++++--- .../src/reserving_mpsc.rs | 21 ++++++++++++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 679a52e2f..c525ea819 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -130,7 +130,7 @@ against the narrower layouts rather than against a producer count. | `Balanced` (default) | 4,294,967,295 | 2^32 | about 37 seconds | | `Enduring` | 65,535 | 2^48 | about 28 days | | `Perpetual` | 255 | 2^56 | about 20 years | -| `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | +| `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | about 5,000 years | The last column is arithmetic, not a measurement: pushes-to-recurrence divided by a sustained rate of about 116 million pushes per second. **That rate predates a @@ -240,8 +240,9 @@ that costs in throughput is not established, while under `Wide` the whole push path was measured as slower as producer count rises -- near parity at one or two, several times by thirty-two, in the isolated regime. What `Wide` provides that the `u64` layouts do not is a 64-bit position: the recurrence moves to -2^64 pushes, which no deployment reaches, rather than to a horizon measured in -years. +2^64 pushes -- about 5,000 years at the same rate the table above uses, rather +than the twenty `Perpetual` buys. That is a longer horizon, not the absence of +one, and it moves with the caller's rate like every other figure in that column. **`experimental-permit-claim`** adds `permit_mpsc`, a different claim protocol in which the decision and the operation are one atomic rather than two. It is diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 9adbde349..2cbb434d2 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -99,7 +99,7 @@ //! | `Balanced` (default) | 4,294,967,295 | 2^32 | about 37 seconds | //! | `Enduring` | 65,535 | 2^48 | about 28 days | //! | `Perpetual` | 255 | 2^56 | about 20 years | -//! | `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | unreachable | +//! | `Wide` (needs `dwcas`) | 4,294,967,295 | 2^64 | about 5,000 years | //! //! The last column is arithmetic, not a measurement: pushes-to-recurrence //! divided by a sustained rate of about 116 million pushes per second. **That @@ -139,8 +139,10 @@ //! thing in //! this crate //! that costs a third-party dependency. What it provides that `Perpetual` does -//! not is a 64-bit position: the recurrence moves to 2^64 pushes, which no -//! deployment reaches, rather than to a horizon measured in years. +//! not is a 64-bit position: the recurrence moves to 2^64 pushes -- about 5,000 +//! years at the rate the table above uses, against the twenty `Perpetual` buys. +//! A longer horizon rather than the absence of one, and it moves with the +//! caller's rate like every other figure in that column. //! //! The default remains `Balanced` so that no existing caller's behaviour //! changed when the choice was introduced. Under it, a queue driven past 2^32 diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index b2b674547..21a099784 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -34,7 +34,8 @@ //! the same word, differing only in shift and mask constants, though what that costs in //! throughput is not established (see [`ClaimLayout`]). [`Enduring`] sits between //! them, and the `dwcas` feature adds a 128-bit word whose 64-bit position moves -//! the recurrence to 2^64 pushes, which no deployment reaches. +//! the recurrence to 2^64 pushes -- about 5,000 years at the rate +//! [`ClaimLayout`] documents, which is a longer horizon rather than no horizon. //! //! ``` //! use windows_waitable_queues::reserving_mpsc::{self, Perpetual}; @@ -95,8 +96,12 @@ //! line every thread in the system touches. //! //! So the two ship as peers ([D-16](../DESIGN-NOTES.md#d-16)): `slotwise_mpsc` for a -//! caller who wants the cheapest possible push and can treat a refusal as -//! backpressure, this shape for a caller with a message it must not lose. That +//! caller who can treat a refusal as backpressure, this shape for a caller with +//! a message it must not lose. The distinction is what each offers, not which +//! one is quicker -- an earlier version of this sentence said `slotwise_mpsc` +//! was for "a caller who wants the cheapest possible push", which asserts a cost +//! ordering this crate does not establish and which its own end-to-end +//! measurement did not find. That //! is the narrow-trait argument from [D-2](../DESIGN-NOTES.md#d-2) reaching //! its sharpest case -- `slotwise_mpsc` does not implement //! [`Reserving`](crate::Reserving) because it genuinely cannot, not because @@ -612,8 +617,8 @@ impl ClaimLayout for Perpetual { /// /// Requires the `dwcas` feature, which is what brings in the `portable-atomic` /// dependency this crate otherwise does not have. The position is 64 bits, so it -/// recurs after 2^64 pushes -- a bound that exists but that no deployment -/// reaches, rather than the twenty years [`Perpetual`] buys. +/// recurs after 2^64 pushes -- about 5,000 years at the rate [`ClaimLayout`] +/// documents, against the twenty [`Perpetual`] buys. Longer, not unbounded. /// /// The whole push path was measured as slower under this layout than under a /// `u64` one, and the difference **grows with producer count** -- near parity @@ -629,8 +634,10 @@ impl ClaimLayout for Perpetual { /// [`Perpetual`] reaches about twenty years on a plain `AtomicU64`, and what /// that costs in throughput is not established -- see [`ClaimLayout`]. What this /// layout provides that the others do not is a 64-bit position: the recurrence -/// moves to 2^64 pushes, which no deployment reaches, rather than to a horizon -/// measured in years. +/// moves to 2^64 pushes -- about 5,000 years at the rate [`ClaimLayout`] +/// documents, rather than the twenty [`Perpetual`] buys. A longer horizon, not +/// the absence of one, and it scales with the caller's rate like the rest of +/// that column. /// /// The reservation ceiling is [`u32::MAX`] rather than the 64 bits the field /// could hold, because the count is reported to callers as a `u32`. From f3d57577d9e4c7e059ff21c04fbf6c5cf096b312 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 21:43:58 -0700 Subject: [PATCH 057/139] fix(platform-probes): a zero numerator rendered 0.00x, which reads as a result Both ratio formatters guarded only the denominator, on the reasoning that division is what breaks. Zero is this probe's sentinel for "this shape did not run", and it means that on either side of the division: a zero denominator printed `inf`, which the guard caught, and a zero numerator printed `0.00x`, which nothing caught. The second is the worse of the two and is why this is a fix rather than tidying. `inf` announces itself as broken. `0.00x` reads as a shape that was immeasurably fast -- a plausible number, in a column of measurements, produced by a row that never ran. Both formatters now require a positive numerator and denominator. Three tests cover it, including one that asserts the two functions agree about what is unmeasurable in every position: they are separate functions with separate guards, which is precisely how one of them came to guard half the cases. Verified by sabotage -- restoring the denominator-only guard renders `0.00x` and fails two tests. Also corrects two dates this branch wrote without an offset, against the "timestamps carry their offset" rule: M4.5's archive group heading and its stub link. The archive's own completion stamp already carried `UTC-07:00`, so the group heading contradicted the entry beneath it. Swept the dates this branch added rather than the two reported; the rest of the repository's bare dates are filenames, directory names and pre-existing history, which that rule does not govern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 2 +- .../COMPLETED-CHECKLIST.md | 2 +- .../src/queue_contention.rs | 23 ++++++-- .../src/queue_contention/tests.rs | 54 +++++++++++++++++++ 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 205b1b683..c7eb61805 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -143,7 +143,7 @@ correctness in the archive. **Also not in scope, because it is done:** emitting the dispersion. See M4.5 below. -- [x] **M4.5** -- Emit the dispersion, not just the median. -> [completed 2026-09-15](COMPLETED-CHECKLIST.md#m45) +- [x] **M4.5** -- Emit the dispersion, not just the median. -> [completed 2026-09-15 UTC-07:00](COMPLETED-CHECKLIST.md#m45) - [ ] **M4.3** -- Close the undrained window at the start of the drained regime with a readiness handshake, and re-measure everything that changes. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 93a02252a..bd7111ea8 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1115,7 +1115,7 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. > **-> DEPENDS ON M2.14.1:** the pointer has nothing to point at until the manifest exists. -## Moved 2026-09-15 -- M4.5: the probe carries its own dispersion +## Moved 2026-09-15 20:04:09 UTC-07:00 -- M4.5: the probe carries its own dispersion ### M4.5 -- Emit the dispersion, not just the median. *(completed 2026-09-15 20:04:09 UTC-07:00)* diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 0ac2ddef1..334675c6e 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -342,10 +342,20 @@ pub fn ratio_bounds(numerator: Run, denominator: Run) -> Option<(f64, f64)> { /// The bound is printed in square brackets to mark it as *not* a sampled range: /// the row ranges above it are observed spans, this is arithmetic over two of /// them. +/// +/// **Both rows must have measured something.** A shape that failed to run +/// reports zero nanoseconds, and zero is the sentinel for "no measurement here" +/// on either side of the division -- a zero numerator would render `0.00x`, +/// which is a number a reader takes for a result rather than for the absence of +/// one. An earlier version guarded only the denominator, on the reasoning that +/// division is what breaks; publishing a plausible figure from a row that never +/// ran is the worse failure of the two. #[must_use] pub fn format_ratio_bounded(numerator: Option, denominator: Option) -> String { match (numerator, denominator) { - (Some(numerator), Some(denominator)) if denominator.nanos_per_op > 0.0 => { + (Some(numerator), Some(denominator)) + if numerator.nanos_per_op > 0.0 && denominator.nanos_per_op > 0.0 => + { let point = numerator.nanos_per_op / denominator.nanos_per_op; match ratio_bounds(numerator, denominator) { Some((low, high)) => format!("{point:.2}x [{low:.2}-{high:.2}]"), @@ -421,13 +431,16 @@ pub fn format_scaling(scaling: Option) -> String { /// `numerator / denominator` as a cost ratio, or `--` when either is missing. /// -/// Guards the denominator rather than trusting it: a shape that failed to run -/// reports zero, and a division by it would print `inf` or `NaN` in a column a -/// reader would otherwise take for a measurement. +/// Guards both rows rather than trusting them: a shape that failed to run +/// reports zero, so a zero denominator would print `inf` or `NaN` and a zero +/// numerator would print `0.00x` -- and of those two the second is the more +/// dangerous, because it looks like a measurement rather than like a failure. #[must_use] pub fn format_ratio(numerator: Option, denominator: Option) -> String { match (numerator, denominator) { - (Some(numerator), Some(denominator)) if denominator.nanos_per_op > 0.0 => { + (Some(numerator), Some(denominator)) + if numerator.nanos_per_op > 0.0 && denominator.nanos_per_op > 0.0 => + { format!("{:.2}x", numerator.nanos_per_op / denominator.nanos_per_op) } _ => "--".to_owned(), diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 50c75068c..6030309ad 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -835,3 +835,57 @@ fn scaling_bounds_is_none_when_a_row_is_missing() { "the one-producer row is absent, so no bound exists" ); } + +/// Zero is the sentinel for "this shape did not run", and it is as meaningful on +/// the numerator side as on the denominator. A zero denominator would render +/// `inf`; a zero numerator renders `0.00x`, which is worse -- `inf` announces +/// itself as broken, and `0.00x` reads as a shape that was immeasurably fast. +#[test] +fn format_ratio_refuses_a_zero_numerator() { + let measured = run(shapes::RESERVING_MPSC, 4, 100_000_000.0); + let absent = run(shapes::SLOTWISE_MPSC, 4, 0.0); + assert_eq!(absent.nanos_per_op, 0.0, "the fixture must have zero cost"); + assert_eq!( + format_ratio(Some(absent), Some(measured)), + "--", + "a row that never ran must not render as a ratio" + ); +} + +#[test] +fn format_ratio_bounded_refuses_a_zero_numerator() { + let measured = run_spanning(shapes::RESERVING_MPSC, 4, 8.0, 10.0, 12.0); + let absent = run_spanning(shapes::SLOTWISE_MPSC, 4, 0.0, 0.0, 0.0); + assert_eq!( + format_ratio_bounded(Some(absent), Some(measured)), + "--", + "a row that never ran must not render as a ratio" + ); +} + +/// Both formatters agree about what is unmeasurable, in both positions. They are +/// separate functions with separate guards, which is exactly how one of them +/// came to guard only half the cases. +#[test] +fn both_ratio_formatters_reject_the_same_unmeasurable_rows() { + let measured = run_spanning(shapes::RESERVING_MPSC, 4, 8.0, 10.0, 12.0); + let absent = run_spanning(shapes::SLOTWISE_MPSC, 4, 0.0, 0.0, 0.0); + for (numerator, denominator) in [ + (Some(absent), Some(measured)), + (Some(measured), Some(absent)), + (Some(absent), Some(absent)), + (None, Some(measured)), + (Some(measured), None), + ] { + assert_eq!( + format_ratio(numerator, denominator), + "--", + "format_ratio accepted an unmeasurable pair" + ); + assert_eq!( + format_ratio_bounded(numerator, denominator), + "--", + "format_ratio_bounded accepted an unmeasurable pair" + ); + } +} From 10ef5fc5784cade86cf7ed030ae117c603a287b2 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 21:43:59 -0700 Subject: [PATCH 058/139] docs: the capture date needs its offset, and the time-figure count moved Both published copies of the capture attribution wrote a bare 2026-09-15. The timestamps-carry-their-offset rule exists so two readers in different zones agree on the instant; a capture attribution is exactly the place that matters, since its whole purpose is to make a figure reusable by someone else. The root design note said the layout tables carry 'two time figures'. They now carry four -- 37 seconds, 28 days, 20 years and the 5,000-year Wide row -- and the fourth is one I added last round while correcting the unreachable claim. My own fix invalidated the sentence counting the thing it changed, two paragraphs away. Reworded to name the constant-derived facts and the time figures without counting either. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 5 +++-- crates/windows-waitable-queues/README.md | 2 +- crates/windows-waitable-queues/src/lib.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 8aefcf4e3..cab2812b3 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1904,9 +1904,10 @@ foreach ($pattern in '\b255\b', '37 seconds', '2\^56', '4,294,967,295', 'about 2 ``` **All of these are restated by hand with nothing checking them.** Three of those facts -- the -ceiling, the span and the field ceiling -- follow from `ClaimLayout`'s associated constants. The two +ceiling, the span and the field ceiling -- follow from `ClaimLayout`'s associated constants. The time figures follow from a field width *and* an assumed sustained push rate, so a -constants-versus-table check would validate those three outright and the time figures only once the +constants-versus-table check would validate the constant-derived facts outright and the time figures +only once the rate is pinned somewhere single. That distinction bounds what the cheapest remedy below can do -- an earlier version of this paragraph said every one was derivable from the constants, which overstated it, in a note about overstatement. The error surface is proportional to how often a fact is restated, diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index c525ea819..43bd2cdb4 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -432,7 +432,7 @@ cannot be omitted again. | Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | | Runs | 3 whole-probe invocations; cells are the median of the three, ranges span all 15 repetitions | | Instrument | `probe-queue-contention`, built from `fecd352` (the commit that added the range columns) | -| Taken | 2026-09-15 | +| Taken | 2026-09-15 UTC-07:00 | The banner's `numa[16]` is a single NUMA node holding all sixteen processors, so nothing here says anything about cross-domain behaviour. `permit_mpsc` is behind diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 2cbb434d2..f13650cd1 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -353,7 +353,7 @@ //! | Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | //! | Runs | 3 whole-probe invocations; cells are the median of the three, ranges span all 15 repetitions | //! | Instrument | `probe-queue-contention`, built from `fecd352` | -//! | Taken | 2026-09-15 | +//! | Taken | 2026-09-15 UTC-07:00 | //! //! [`D-observations-not-verdicts`]: https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/DESIGN-NOTES.md#d-observations-not-verdicts //! From 9340fcfb11ae280c8dab8437dc6c762f06e01914 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 22:02:53 -0700 Subject: [PATCH 059/139] docs: apply the one-home rule to the prose that introduced it Ten findings, all of them this PR's new documentation rule turned on the PR that added it. That is the right outcome and most of them are correct. The measurement table had two hand-maintained homes -- the README and the crate rustdoc -- which is precisely what the rule forbids, and this branch had already demonstrated the failure: a retake updated the README and left the rustdoc carrying the superseded capture for two commits. The rustdoc copy is gone. It now carries the claims that do not drift (one host's observation, not a ranking; the ranges matter more than the medians; what an operation is in each row; the host is a single NUMA node so nothing there speaks to cross-domain behaviour) and links to the README, which owns the figures and their attribution. The README's measurement section gained a real heading so the link has a stable anchor. Six derived proportions removed, each restating data shown beside it: "a factor of three" over 19.3 to 59.5, in three files "a spread of 1.33x" over 181.5-242.3, in two "which is 93%" over 57 of 61 -- inside the paragraph explaining why not to do this The last one is the one to sit with. The rule's own worked example computed a ratio over the counts it was citing as a cautionary tale about computing ratios over counts. Removing it costs nothing: the counts were already there, and the reader can see 57 of 61 without being told what fraction that is. Two stale figures were serving as illustrative examples of "a number" in the rule itself -- both medians from the superseded pre-correction capture this PR removed from the documentation. Using withdrawn measurements as decoration reintroduces them without attribution. Replaced with descriptions of the kinds of number. D-41 said the figures live in the probe's note "rather than restated here" and then restated them. Removed. Sweeping that claim found it in five more places -- the README twice, the crate rustdoc twice, and ClaimLayout's rustdoc -- all now stating the qualitative conclusion and leaving the magnitudes to the probe note that owns them. D-35 presented pre-correction magnitudes as current, in its index row and in its section. Both now carry the status adjacent to the claim. The review asked only about the index row; marking the section too is the same pairing D-17 and D-27 needed, and missing the section half is a mistake this branch has now made three times. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 10 +- .../COMPLETED-CHECKLIST.md | 2 +- .../windows-waitable-queues/DESIGN-NOTES.md | 10 +- crates/windows-waitable-queues/README.md | 12 ++- crates/windows-waitable-queues/src/lib.rs | 100 +++++++----------- .../src/reserving_mpsc.rs | 2 +- 6 files changed, 60 insertions(+), 76 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index cab2812b3..1a1dd0804 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1977,8 +1977,8 @@ README and the crate rustdoc disagreeing because one was retaken; an attribution the figures no longer came from; one recurrence horizon left unqualified across seven sites in three wordings; a withdrawn magnitude surviving in two public rustdocs. The most instructive was a proportion that restated two counts **given four words earlier in the same sentence** and got one of -them wrong -- "in both cases roughly 60%", against 57 of 61, which is 93%. The data was adjacent and -the summary of it was false, because prose is not checkable and nobody checks it. +them wrong -- it said "in both cases roughly 60%" where one of the two cases was 57 of 61. The data +was adjacent and the summary of it was false, because prose is not checkable and nobody checks it. **This repository already contains the better pattern and did not apply it here.** `mutation-sweeps/2026-09-02/` is a dated, committed capture directory: data as an artifact, cited @@ -1992,9 +1992,9 @@ So the principle, which holds regardless of which mechanism is eventually chosen contention, and the spread is wide enough that the ordering is a flag rather than a finding" is a claim. It contains no digits, so it cannot drift from the data -- it can only be wrong about it, which a reader can see. -- **A number belongs in an artifact.** `15.3`, `246.9`, `fecd352`, a count of occurrences: one copy, - with its provenance travelling *with* it rather than in a hand-maintained attribution table - beside it. +- **A number belongs in an artifact.** A measured cost, a capture's commit, a count of occurrences: + one copy, with its provenance travelling *with* it rather than in a hand-maintained attribution + table beside it. - **A proportion over data we hold is not a finding, it is a restatement of one.** Computed by hand, checked by nobody, and stale the moment any input moves. The counts are the finding. A reader who wants a ratio can take one, against a denominator they chose and at a moment they know. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index bd7111ea8..2cc2f99eb 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1142,5 +1142,5 @@ Nine tests cover it, verified load-bearing by sabotage: taking the fastest from instead of the minimum fails `median_run_carries_the_fastest_and_slowest_repetitions`. The dispersion justified itself on first capture. `slotwise_mpsc` at two producers spans 19.3 to -59.5 ns/op -- a factor of three within one configuration on one host -- which the median alone had +59.5 ns/op within one configuration on one host, which the median alone had concealed entirely, in a table that had already been published twice. diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index cd3de2ed0..fdd05e81a 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -69,13 +69,13 @@ preferred. | D-32 | **`Reserving::Reservation<'a>` gains a bound, before the crate publishes.** The associated type is currently unbounded, so a caller generic over the trait can claim a slot and drop it but never redeem it -- the trait cannot express the operation it exists for. Both implementors already have identical `send` and `is_disconnected` signatures, so the bound is additive; adding it after publication is a breaking change to every implementor. Done as SH-1.5: the [`Claim`](src/traits.rs) trait carries `send` and `is_disconnected`, and both reservation types implement it as forwarders. `Claim` must be in scope to call those methods on a claim whose concrete type the caller has not named, which is why it is re-exported at the crate root. | | D-33 | **`PushError` is `#[non_exhaustive]`, and the one-directional doorbell is disclosed rather than fixed before 0.1.0.** The receive-side errors already carried the attribute and the send side lacked it by omission; adding it after publication is itself breaking, so it is taken now while the crate has no external consumers. Whether a producer can *wait* for room stays open as M32.3 -- it is additive, so it does not gate the release -- but the absence is stated in both the crate docs and the README, because `crossbeam-channel`'s `send` blocks and a reader arriving from it will assume this one does too. | | D-34 | **Every bounded queue surveyed is ABA-safe for one of two reasons, and this crate's `reserving_mpsc` has neither.** Either the claim counter is a whole machine word, so recurrence is unreachable -- crossbeam, concurrent-queue, thingbuf, Vyukov, SCQ's `Head`/`Tail` -- or the authorizing compare-exchange is moved onto the cell, so the decision and the write are validated together (CRQ, SCQ). Ours packs the position into a 32-bit *subfield* and authorizes with an exchange that does not cover the separately-read `head`. Nikolaev (DISC 2019, section 3) states the width assumption the field relies on and states it for **CPU-word** width, which a subfield does not satisfy; DPDK's `rte_ring` is the same protocol as ours and its published justification covers modular arithmetic only. The generalisation -- ours, unstated in any source -- is that **the atomic operation authorizing the write must cover everything the decision depended on.** Survey in [DESIGN-SESSION-2026-09-02](design-sessions/DESIGN-SESSION-2026-09-02-claim-protocol-prior-art.md); the fix is the claim-protocol replacement recorded there. | -| D-35 | **Measured: the permit claim is 2.7x faster than `reserving_mpsc` at 16-32 producers, and 1.45x slower at one.** The safer claim is also the faster one everywhere contention exists, which was not the expected result -- it touches *two* shared lines where the shipping shape touches one plus a read, and [D-26](#d-26) had established that the shared line is what collapses. The mechanism is that both of its operations are unconditional read-modify-writes that never retry, where the shipping shape's compare-exchange retries once per lost race; the retries dominate long before the second line does. It is the only shape measured that gets *faster* per push as producers are added (42.8 ns at two to 19.5 at thirty-two) and the only one that stays within 1.5x of a bare contended `fetch_add`. **This decides the shape of the fix but not the fix**: the drained regime's refusal counts differ by orders of magnitude in a way this harness cannot attribute, which SH-15.5.1 exists to settle before SH-15.6 adopts anything. | +| D-35 | **The magnitudes below predate the timing correction and are retained as the record of what the measurement showed, not as current figures.** **Measured: the permit claim is faster than `reserving_mpsc` at high producer counts and slower at one** -- originally recorded as 2.7x and 1.45x respectively, from a capture taken before the probe's timing window was corrected; per [D-29](#d-29) no current public figure derives from that capture. The direction survives the correction; the magnitudes do not. The safer claim is also the faster one everywhere contention exists, which was not the expected result -- it touches *two* shared lines where the shipping shape touches one plus a read, and [D-26](#d-26) had established that the shared line is what collapses. The mechanism is that both of its operations are unconditional read-modify-writes that never retry, where the shipping shape's compare-exchange retries once per lost race; the retries dominate long before the second line does. It is the only shape measured that gets *faster* per push as producers are added (42.8 ns at two to 19.5 at thirty-two) and the only one that stays within 1.5x of a bare contended `fetch_add`. **This decides the shape of the fix but not the fix**: the drained regime's refusal counts differ by orders of magnitude in a way this harness cannot attribute, which SH-15.5.1 exists to settle before SH-15.6 adopts anything. | | D-36 | **Superseded by [D-41](#d-41): the hazard is now a layout choice, not a defect that must ship.** The reasoning below stands as the record of why it was right to disclose rather than delay while the only known fix was the claim-protocol replacement. **0.1.0 ships SH-14.1 disclosed rather than fixed, and the disclosure is a release blocker.** Following [D-31](#d-31)'s principle -- the disclosure, not the deferral, is the decision -- because the fix is a claim-protocol replacement ([D-35](#d-35)) whose adoption is still gated on an open question, and holding the release for it would trade a *documented* hazard for an undocumented rush. **The two gaps are not equally forgiving and the text says so**: an unverified ordering is a risk of a bug, this is a known one with a computed exposure, and its failure mode is silent -- no error, panic, or counter -- so a caller can neither detect nor mitigate it. That is precisely why it may not ship in silence. Stated in the crate docs, the README, and the shape's own module docs, each leading with **"on every target, not only 32-bit ones"**, because the natural spelling "32-bit position" invites the opposite reading and SH-6.1 already had to be corrected for exactly that. The shape-selection guidance in both documents was also amended: it previously said "start with `reserving_mpsc`" with no caveat, pointing callers at the hazardous shape by default. | | D-37 | **Partly superseded by [D-41](#d-41): the wide word ships as a *layout* behind the non-default `dwcas` feature, not as a separate `reserving_mpsc_wide` shape, and the gate is the feature rather than the target.** What stands is the reasoning below about `portable-atomic`: `default-features = false` is load-bearing, because with defaults on it silently substitutes a global lock, and D-7's burden of proof is discharged rather than waived. What does not is the shape's name and the premise that the narrow word must keep SH-14.1 -- re-apportioning the narrow word moves the exposure to a later recurrence (`2^48` under `Enduring`, `2^56` under `Perpetual`) without a third-party dependency -- what it costs in throughput is unestablished, see [D-41](#d-41) -- so the wide word is no longer the only way out. **The reserving claim word ships in two widths: the narrow one on every target, the wide one only where a 128-bit exchange is genuinely lock-free.** `reserving_mpsc` keeps its packed 64-bit word, keeps SH-14.1's hazard, keeps [D-36](#d-36)'s warnings, and is **never silently swapped** for the wide shape on targets that could host one -- a contract that changes with the target is what PLATFORM INTEGRITY rule 2 forbids, and a caller who read "2^32" must get 2^32. `reserving_mpsc_wide` is the same protocol with a `u128` word split 64/64: recurrence needs 2^64 pushes, and the capacity ceiling rises to 2^62. **The gate is one line of `Cargo.toml`: `default-features = false`.** Measured, not designed -- with the default feature set `portable-atomic` compiles on i686 and silently substitutes a global lock, but with defaults off `AtomicU128` **does not exist** there (`no AtomicU128 in the root`), nor on x86_64 built without `cmpxchg16b`. It exists exactly where a native lock-free exchange is guaranteed at compile time, so the `use` statement is the gate and it fails loudly. A `cfg(target_has_atomic = "128")` would be the *wrong* gate -- it is emitted even with `cmpxchg16b` disabled -- and a `const` assertion on `is_always_lock_free()`, though genuinely const-evaluable, is redundant where the type exists and unreachable where it does not. That is the standard SH-14.2 already set when it probed i686 to confirm `AtomicU64` was lock-free before widening `slotwise_mpsc`, recording that a hidden mutex "would have made this a bad trade". [D-7](#d-7)'s burden of proof for adding a Cargo feature is **discharged, not waived**: D-7 rejected feature-gating because the only benefit was compile time, and the cost here is a third-party dependency, which dead-code elimination does not remove from `Cargo.lock` or from an auditor's review. | | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host: a flag to measure locally, not a finding. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). Which half the bits go to is the trade this decision exposes rather than settles: a 32-bit reservation field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, against a position whose width sets the recurrence horizon. The bound that matters on the reservation side is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. Which of the two a deployment needs is the deployment's question. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts sit outside the same-code control but too close to it to establish an ordering or a cost on this host: a flag to measure locally, not a finding. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). Which half the bits go to is the trade this decision exposes rather than settles: a 32-bit reservation field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, against a position whose width sets the recurrence horizon. The bound that matters on the reservation side is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. Which of the two a deployment needs is the deployment's question. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered @@ -1295,6 +1295,12 @@ obviously cheaper. This decision records only the landscape and the criterion. ## D-35: the permit claim measured, and the result that inverts the expectation +**Every magnitude in this section predates the correction to the probe's timing window, and is +retained as the record of what was measured rather than as a current figure.** Per +[D-29](#d-29) no current public figure derives from that capture. The direction the section +establishes -- that the permit claim is faster where contention exists and slower at one producer -- +survives the correction; the multipliers do not. + Run by `probe-queue-contention` on the reference host (x86-64, 16 logical / 8 physical, SMT on), release build, five repetitions per configuration with the median kept. The whole run was repeated three times; the isolated numbers reproduced within noise except one outlier noted below. diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 43bd2cdb4..d025b465f 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -160,7 +160,7 @@ let (tx, rx) = reserving_mpsc::bounded_as::(64)?; `Enduring`, and `Perpetual` all issue the same exchange on the same 64-bit word and differ only in shift and mask constants, so there is no structural reason for one to be slower -- but **what that costs in throughput is not established**: a -probe comparing them found them indistinguishable at low producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. `Wide` is a separate matter: it needs a 128-bit exchange, +probe comparing them found them indistinguishable at low producer counts, and at high counts sat outside the probe's same-code control but too close to it to establish an ordering or a cost on this host. `Wide` is a separate matter: it needs a 128-bit exchange, and the whole push path was measured as slower under it as producer count rises -- near parity at one or two, several times by thirty-two, in the isolated regime -- and it is the only thing in @@ -206,7 +206,7 @@ proportionally longer to reach its wrap. - **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty years out. **What it costs in throughput is not established** -- it issues the same atomic compare-exchange on the same `u64` as the default, and was measured - as indistinguishable from it at low producer counts; at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. + as indistinguishable from it at low producer counts; at high counts sat outside the probe's same-code control but too close to it to establish an ordering or a cost on this host. - **`slotwise_mpsc` does not have this hazard** under any layout. Its positions are 64 bits on every target, so the equivalent wrap needs 2^64 claims. It does not offer `Reserving`. @@ -397,7 +397,9 @@ The measurements below are one host's observation, recorded with the parameters that produced them. They are not a ranking, and which shape suits a given deployment is the deployment's question. -**What was measured**, in ns per operation, isolated regime (producers only, +### What was measured + +In ns per operation, isolated regime (producers only, capacity large enough that nothing is refused). Each cell is the median of three whole-probe runs, followed by the full range across all fifteen repetitions those runs contain. An operation is one successful push for the three queue shapes; @@ -414,7 +416,7 @@ labelled per operation rather than per push: | 32 | 224.7 (131.4-268.3) | 51.3 (40.7-55.4) | 21.9 (20.7-39.0) | 15.0 (14.7-15.7) | **The ranges are the point, not a footnote.** `slotwise_mpsc` at two producers -spans 19.3 to 59.5 -- a factor of three within one configuration on one host -- +spans 19.3 to 59.5 within one configuration on one host, and at thirty-two, 131.4 to 268.3. A median quoted without that is an anecdote, which is why [D-observations-not-verdicts](../windows-platform-probes/DESIGN-NOTES.md#d-observations-not-verdicts) @@ -446,7 +448,7 @@ this host's 8 physical cores, and the spread is not small at either scale. *Between* runs: `slotwise_mpsc` at sixteen producers gave whole-run medians of 225.7, 218.0 and 192.9. *Within* a run the probe reports its own per-row spread -- a fourth, separate invocation of the same build gave that row a median of -226.5 over a 181.5-242.3 range, a spread of 1.33x across its five repetitions. +226.5 over a 181.5-242.3 range across its five repetitions. The parenthesised ranges in the table above are the wider quantity: the extremes over all fifteen repetitions of the three captured runs. The probe's same-code control has been measured at 0.68-1.27x over seven runs, which is wide enough to diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index f13650cd1..a4cd07132 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -132,7 +132,7 @@ //! word and differ only in shift and mask constants, so there is no structural //! reason for one to be slower -- but **what that costs in throughput is not //! established**: a probe comparing them found them indistinguishable at low -//! producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. `Wide` is a separate +//! producer counts, and at high counts sat outside the probe's same-code control but too close to it to establish an ordering or a cost on this host. `Wide` is a separate //! matter: it needs a 128-bit exchange, and the whole push path was measured as //! slower under it as producer count rises -- near parity at one or two, //! several times by thirty-two, in the isolated regime -- and it is the only @@ -186,7 +186,7 @@ //! - **Naming a layout moves it.** `Perpetual` puts the recurrence about twenty //! years out. **What it costs in throughput is not established** -- it issues //! the same atomic compare-exchange on the same `u64` as the default, and was -//! measured as indistinguishable from it at low producer counts; at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. +//! measured as indistinguishable from it at low producer counts; at high counts sat outside the probe's same-code control but too close to it to establish an ordering or a cost on this host. //! - **[`slotwise_mpsc`] does not have this hazard** under any layout. Its //! positions are 64 bits on every target, so the equivalent wrap needs 2^64 //! claims. It does not offer [`Reserving`]. @@ -320,68 +320,44 @@ //! - **[`spsc`] requires exactly one producer and one consumer**, and does less //! work than either MPSC shape because of it. //! -//! The measurements below are one host's observation, recorded with the -//! parameters that produced them. They are not a ranking. -//! -//! Isolated regime (producers only, capacity large enough that nothing is -//! refused), ns per operation. Each cell is the median of three whole-probe -//! runs, followed by the full range across all fifteen repetitions those runs -//! contain -- [`D-observations-not-verdicts`] obliges a published figure to -//! carry its run count *and* its dispersion, and the ranges are the more useful -//! half: `slotwise_mpsc` at two producers spans a factor of three within one -//! configuration on one host. -//! -//! An operation is one successful push for the three queue shapes; for -//! `baseline_fetch_add` it is one `fetch_add`, which is why the column is -//! labelled per operation rather than per push. -//! -//! | producers | `slotwise_mpsc` | `reserving_mpsc` | `permit_mpsc` | `baseline_fetch_add` | -//! |---|---|---|---|---| -//! | 1 | 6.3 (6.3-7.5) | 5.4 (5.4-6.4) | 7.9 (7.9-8.3) | 2.3 (2.3-2.7) | -//! | 2 | 50.6 (19.3-59.5) | 31.9 (22.5-35.2) | 44.2 (37.4-45.9) | 12.1 (5.8-14.3) | -//! | 4 | 91.6 (89.7-99.9) | 37.2 (31.6-41.5) | 31.8 (30.4-32.9) | 13.8 (12.6-17.6) | -//! | 8 | 138.6 (126.9-157.6) | 37.8 (34.3-41.7) | 25.9 (25.0-27.4) | 14.7 (13.9-15.9) | -//! | 16 | 218.0 (188.9-272.7) | 47.9 (44.9-56.0) | 21.8 (20.9-25.6) | 14.8 (14.4-15.9) | -//! | 32 | 224.7 (131.4-268.3) | 51.3 (40.7-55.4) | 21.9 (20.7-39.0) | 15.0 (14.7-15.7) | -//! -//! Attribution, because a figure without it is not reusable data: -//! -//! | | | -//! |---|---| -//! | Host | `x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16]` | -//! | Profile | release | -//! | Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | -//! | Runs | 3 whole-probe invocations; cells are the median of the three, ranges span all 15 repetitions | -//! | Instrument | `probe-queue-contention`, built from `fecd352` | -//! | Taken | 2026-09-15 UTC-07:00 | -//! +//! **The measurements live in one place, not two.** This crate's +//! [README][readme-measurements] carries the capture: the isolated-regime table, +//! every cell's observed range, and the attribution -- host banner, profile, +//! sampling parameters, run count, the instrument's commit, and when it was +//! taken. That is deliberately not duplicated here, because a figure with two +//! hand-maintained homes is a figure that will disagree with itself the first +//! time one of them is retaken. This branch did exactly that once already. +//! +//! What is worth saying without the digits: +//! +//! - The figures are **one host's observation**, not a ranking, and which shape +//! suits a deployment is the deployment's question. +//! - **The ranges matter more than the medians.** Every cell carries the span +//! its repetitions covered, because +//! [`D-observations-not-verdicts`] obliges a published figure to arrive with +//! its run count *and* its dispersion. At some producer counts that span is +//! wide enough to swallow the difference between shapes. +//! - An *operation* is one successful push for the three queue shapes; for +//! `baseline_fetch_add` it is one `fetch_add` on a shared `AtomicU64`, which is +//! why the column is labelled per operation rather than per push. It is +//! included so the queue figures can be read against what this processor does +//! to a contended line at all. +//! - The host is a single NUMA node holding all its processors, so **nothing +//! there says anything about cross-domain behaviour**, and producer counts +//! above its physical core count oversubscribe it. +//! - `permit_mpsc` is behind `experimental-permit-claim` and is outside the +//! semver promise. +//! +//! An earlier capture compared two machines and was removed rather than carried +//! forward: its figures predate a correction to the probe's timing window, and +//! neither machine is available here to retake them. One finding from it was +//! structural rather than numeric and is worth keeping -- the split was designed +//! on the assumption that `slotwise_mpsc` would be the cheaper shape, and +//! measurement disagreed on both machines. +//! +//! [readme-measurements]: https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-waitable-queues/README.md#what-was-measured //! [`D-observations-not-verdicts`]: https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/DESIGN-NOTES.md#d-observations-not-verdicts //! -//! The banner's `numa[16]` is a single NUMA node holding all sixteen processors, -//! so nothing here says anything about cross-domain behaviour. `permit_mpsc` is -//! behind `experimental-permit-claim` and is not covered by the semver promise. -//! `baseline_fetch_add` is N threads incrementing one `AtomicU64`, included so -//! the queue figures can be read against what this processor does to a contended -//! line at all. -//! -//! **Read these as one machine's numbers.** Producer counts above 8 oversubscribe -//! this host's 8 physical cores, and the spread is not small at either scale. -//! *Between* runs: `slotwise_mpsc` at sixteen producers gave whole-run medians -//! of 225.7, 218.0 and 192.9. *Within* a run the probe reports its own per-row -//! spread -- a fourth, separate invocation of the same build gave that row a -//! median of 226.5 over a 181.5-242.3 range, a spread of 1.33x across its five -//! repetitions. The parenthesised ranges in the table above are the wider -//! quantity: the extremes over all fifteen repetitions of the three captured -//! runs. -//! -//! A previous version of this table compared an AMD EPYC 7763 slice against a -//! Snapdragon X2 Elite. It was removed rather than carried forward: its figures -//! predate a correction to the probe's timing window, and neither machine is -//! available here to retake them. One finding from it was structural rather than -//! numeric and is worth keeping -- the split was designed on the assumption that -//! `slotwise_mpsc` would be the cheaper shape, and measurement disagreed on both -//! machines. -//! //! **What moves these numbers.** Producer count, how hard the consumer drains, //! and where the threads are scheduled -- placement alone moved an SPSC handoff //! by 5.6x on an earlier host this workspace measured. diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 21a099784..2d518c3d2 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -263,7 +263,7 @@ use crate::options::Options; /// three issue the same atomic compare-exchange on the same `u64` and differ /// shift and mask constants, so there is no structural reason for one to be /// slower. **What that costs in throughput is not established**: a probe -/// comparing them found them indistinguishable at low producer counts, and at high counts ran 1.23-1.30x the default against a same-code control that itself reaches 1.12x -- outside the control, but too close to it to establish an ordering or a cost on this host. The settled trade is the +/// comparing them found them indistinguishable at low producer counts, and at high counts sat outside the probe's same-code control but too close to it to establish an ordering or a cost on this host. The settled trade is the /// reservation ceiling; throughput is target-dependent and this crate does not /// characterise it beyond the one host in the note above. /// From 28978baacca05c2e1301ad098febaf1849085c91 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 22:41:26 -0700 Subject: [PATCH 060/139] fix(platform-probes): an unmeasured spread rendered as perfect stability `Run::spread()` returned `0.0` when a row measured nothing, on the reasoning that zero is not a plausible spread so it could serve as a sentinel. The report renders it through `format_scaling`, which rejects only non-finite values, so that row printed `1.00x`'s more flattering neighbour: **`0.00x`**, the most reassuring value the column can hold, meaning every repetition agreed exactly -- produced by a shape that never ran. This is the third time on this branch that a zero sentinel reached a reader as a plausible number, after `format_ratio` and `format_ratio_bounded` two commits ago. Fixing those two and leaving this one is the same sibling-blindness the round before flagged: I corrected the functions the review named and not the accessor feeding a third call site. `spread()` now returns `Option`, which removes the sentinel rather than guarding it. Every neighbouring accessor -- `scaling`, `scaling_bounds`, `ratio_bounds` -- already returns `Option` for exactly this situation; `spread` was the one exception, and the exception is what a renderer got wrong. A type that cannot express "no measurement" forces every caller to remember a convention, and one of them will not. Two tests: the accessor answers `None`, and the renderer marks it `--`. The second exists because the accessor is not where the damage happened. Verified by sabotage -- restoring the sentinel behind the `Option` renders `Some(0.0)` and fails. Report re-rendered from a release build; measured rows are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/queue_contention.rs | 20 ++++++--- .../src/queue_contention/tests.rs | 41 +++++++++++++++---- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 334675c6e..3e1598e82 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -206,14 +206,22 @@ impl Run { /// says the figure beside it is one draw from a distribution this host does /// not hold still, which is the reading the median alone hides. /// - /// Zero when the fastest repetition took no measurable time, which cannot - /// happen for a real run and is reported rather than divided by. + /// `None` when the fastest repetition took no measurable time, which cannot + /// happen for a real run and marks a shape that did not run at all. + /// + /// **This returns an `Option` rather than a sentinel, and that is the whole + /// point.** An earlier version returned `0.0` for the unmeasurable case, + /// reasoning that zero is not a plausible spread. It renders as `0.00x`, + /// which reads as *perfect stability* -- the most reassuring cell the column + /// can contain, produced by a row that measured nothing. Every neighbouring + /// accessor already returns `Option` for the same situation; this one was + /// the exception, and the exception is what a renderer got wrong. #[must_use] - pub fn spread(&self) -> f64 { + pub fn spread(&self) -> Option { if self.fastest_nanos_per_op > 0.0 { - self.slowest_nanos_per_op / self.fastest_nanos_per_op + Some(self.slowest_nanos_per_op / self.fastest_nanos_per_op) } else { - 0.0 + None } } } @@ -408,7 +416,7 @@ pub fn render_table(out: &mut dyn fmt::Write, runs: &[Run]) { "{:.1}-{:.1}", run.fastest_nanos_per_op, run.slowest_nanos_per_op ), - format_scaling(Some(run.spread())), + format_scaling(run.spread()), ); } } diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 6030309ad..16a3ef40f 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -628,7 +628,7 @@ fn spread_is_the_slowest_over_the_fastest() { let mut run = run(shapes::RESERVING_MPSC, 4, 100_000_000.0); run.fastest_nanos_per_op = 10.0; run.slowest_nanos_per_op = 13.0; - assert!((run.spread() - 1.3).abs() < 1e-9, "got {}", run.spread()); + assert!((run.spread().expect("a measured row") - 1.3).abs() < 1e-9, "got {:?}", run.spread()); } /// A configuration whose repetitions all took the same time has a spread of @@ -638,21 +638,26 @@ fn spread_of_an_identical_sample_is_one() { let mut run = run(shapes::RESERVING_MPSC, 4, 100_000_000.0); run.fastest_nanos_per_op = 42.0; run.slowest_nanos_per_op = 42.0; - assert!((run.spread() - 1.0).abs() < 1e-9, "got {}", run.spread()); + assert!((run.spread().expect("a measured row") - 1.0).abs() < 1e-9, "got {:?}", run.spread()); } /// A shape that failed to run reports zero, and dividing by it would put `inf` /// in a column a reader takes for a measurement -- the same guard /// `format_ratio` carries. +/// A row that never ran has no spread to report. It used to answer `0.0` here, +/// which the report rendered as `0.00x` -- the most reassuring value the column +/// can hold, meaning "perfectly stable", produced by a shape that measured +/// nothing. `None` is the honest answer and the renderer turns it into `--`. #[test] -fn spread_of_a_zero_sample_is_zero_rather_than_infinite() { +fn spread_of_a_zero_sample_is_none_rather_than_a_reassuring_number() { let mut run = run(shapes::SLOTWISE_MPSC, 4, 0.0); run.fastest_nanos_per_op = 0.0; run.slowest_nanos_per_op = 0.0; - assert_eq!(run.spread(), 0.0); - assert!( - run.spread().is_finite(), - "the spread must never be infinite" + assert_eq!(run.spread(), None, "an unmeasured row has no spread"); + assert_eq!( + format_scaling(run.spread()), + "--", + "and it must not render as a number" ); } @@ -889,3 +894,25 @@ fn both_ratio_formatters_reject_the_same_unmeasurable_rows() { ); } } + +/// The renderer is where the sentinel did its damage, so the guard is asserted +/// there and not only on the accessor. A row that measured nothing must show +/// `--` in the spread column rather than a number a reader would take for +/// stability. +#[test] +fn render_table_marks_an_unmeasured_spread_rather_than_printing_zero() { + let mut absent = run(shapes::SLOTWISE_MPSC, 8, 0.0); + absent.fastest_nanos_per_op = 0.0; + absent.slowest_nanos_per_op = 0.0; + let mut out = String::new(); + render_table(&mut out, &[absent]); + let row = out.lines().nth(1).expect("one row was rendered"); + assert!( + !row.contains("0.00x"), + "an unmeasured row rendered a spread that reads as perfect stability: {row:?}" + ); + assert!( + row.contains("--"), + "an unmeasured spread must be marked: {row:?}" + ); +} From ad2491b93f5b529c0e04e2efeb1d64c1d27f7479 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 22:47:43 -0700 Subject: [PATCH 061/139] style(platform-probes): reflow two assertions cargo fmt wanted wrapped The previous commit edited these two lines through PowerShell and pushed without re-running cargo fmt, so CI's rustfmt job failed on line length. The repository's pre-commit gate requires fmt then clippy whenever a staged file is .rs; I ran the encoding and commit-scope checks and skipped that one, because the change looked like a test edit rather than a code edit. The gate does not make that distinction and is right not to. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/queue_contention/tests.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 16a3ef40f..f188953d5 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -628,7 +628,11 @@ fn spread_is_the_slowest_over_the_fastest() { let mut run = run(shapes::RESERVING_MPSC, 4, 100_000_000.0); run.fastest_nanos_per_op = 10.0; run.slowest_nanos_per_op = 13.0; - assert!((run.spread().expect("a measured row") - 1.3).abs() < 1e-9, "got {:?}", run.spread()); + assert!( + (run.spread().expect("a measured row") - 1.3).abs() < 1e-9, + "got {:?}", + run.spread() + ); } /// A configuration whose repetitions all took the same time has a spread of @@ -638,7 +642,11 @@ fn spread_of_an_identical_sample_is_one() { let mut run = run(shapes::RESERVING_MPSC, 4, 100_000_000.0); run.fastest_nanos_per_op = 42.0; run.slowest_nanos_per_op = 42.0; - assert!((run.spread().expect("a measured row") - 1.0).abs() < 1e-9, "got {:?}", run.spread()); + assert!( + (run.spread().expect("a measured row") - 1.0).abs() < 1e-9, + "got {:?}", + run.spread() + ); } /// A shape that failed to run reports zero, and dividing by it would put `inf` From df16f01cd965fa419373b64536ccc37df02fcb25 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 23:12:45 -0700 Subject: [PATCH 062/139] fix(probes): stop a shape that never ran from publishing figures A row that did not run reports zero, and zero is not an obviously broken value in any of this report's columns: `0.0` ns/op reads as immeasurably fast, `0.00x` as a ratio of one, `0.0-0.0` as perfect stability. Three renderers guarded the sentinel and four did not, so the same row was refused a ratio and granted a cost. That asymmetry is the residue of fixing `Run::spread` at the instance rather than the class: the guard was a convention each renderer restated, so a renderer could omit it by saying nothing. `Run::is_measured` makes it a definition every renderer asks, and `render_table` now marks every measured cell of an unmeasured row rather than only its spread. The producer count and shape survive, being configuration rather than measurement. Also widens the layout table's ratio columns, which allocated 10 characters to a formatter whose ordinary output is 19. A Rust width is a minimum, so the value was not truncated -- it pushed the next two columns out of line with their headers, silently, in every report the probe has emitted. The width is now derived from a named constant that a test holds against the formatter, and the value columns say `ns/op` rather than `ns`, which is the unit they carry. Verified by sabotage: reverting `is_measured` to `true` fails 8 tests, reverting either scaling guard fails 2, and restoring the old column width fails with `"10.00x [6.67-15.00]" is 19 characters ... which allows 10`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 25 +-- .../src/queue_contention.rs | 97 +++++++++-- .../src/queue_contention/tests.rs | 157 ++++++++++++++++++ 3 files changed, 251 insertions(+), 28 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 8be3b286d..e0bf81f4d 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -13,8 +13,8 @@ //! cannot separate. use windows_platform_probes::queue_contention::{ - DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, format_nanos, - format_ratio_bounded, format_scaling_bounded, measure, render_table, shapes, + DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, RATIO_COLUMN_WIDTH, REPETITIONS, + format_nanos, format_ratio_bounded, format_scaling_bounded, measure, render_table, shapes, }; use windows_platform_probes::report::emit_report; @@ -345,8 +345,9 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " 8/56 merely deferring it. Not the exchange in isolation.\n" + " 8/56 moving it to 2^56. Both defer the recurrence rather than" ); + let _ = writeln!(out, " removing it. Not the exchange in isolation.\n"); for (label, regime) in [ ("isolated", &observation.isolated), ("drained", &observation.drained), @@ -354,15 +355,16 @@ fn render(out: &mut dyn std::fmt::Write) { let _ = writeln!(out, " -- {label} --"); let _ = writeln!( out, - " {:<10} {:>11} {:>11} {:>11} {:>11} {:>10} {:>10} {:>10}", + " {:<10} {:>11} {:>11} {:>11} {:>11} {:>w$} {:>w$} {:>w$}", "producers", - "32/32 ns", - "16/48 ns", - "8/56 ns", - "64/64 ns", + "32/32 ns/op", + "16/48 ns/op", + "8/56 ns/op", + "64/64 ns/op", "16/48 vs", "8/56 vs", - "64/64 vs" + "64/64 vs", + w = RATIO_COLUMN_WIDTH ); for &producers in PRODUCER_COUNTS { let narrow = observation.find(regime, shapes::CLAIM_NARROW, producers); @@ -371,7 +373,7 @@ fn render(out: &mut dyn std::fmt::Write) { let wide = observation.find(regime, shapes::CLAIM_WIDE, producers); let _ = writeln!( out, - " {:<10} {:>11} {:>11} {:>11} {:>11} {:>10} {:>10} {:>10}", + " {:<10} {:>11} {:>11} {:>11} {:>11} {:>w$} {:>w$} {:>w$}", producers, format_nanos(narrow), format_nanos(deep), @@ -379,7 +381,8 @@ fn render(out: &mut dyn std::fmt::Write) { format_nanos(wide), format_ratio_bounded(deep, narrow), format_ratio_bounded(perpetual, narrow), - format_ratio_bounded(wide, narrow) + format_ratio_bounded(wide, narrow), + w = RATIO_COLUMN_WIDTH ); } let _ = writeln!(out); diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 3e1598e82..d4abff5c9 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -200,6 +200,27 @@ pub struct Run { } impl Run { + /// Whether this row carries a measurement at all. + /// + /// **This is the single definition of the "did not run" sentinel, and every + /// renderer asks it rather than restating the test.** A shape that did not + /// run reports zero, and zero is not an obviously broken value in any of + /// this report's columns: `0.0` ns/op reads as immeasurably fast, `0.00x` + /// as a ratio of one, `0.0-0.0` as perfect stability. Each is the most + /// flattering cell its column can hold, produced by a row that measured + /// nothing. + /// + /// It lives here because the test was previously written inline in the + /// renderers that remembered it and simply absent from those that did not + /// -- which is how [`Run::spread`] came to render `0.00x` for a shape that + /// never ran. Fixing that one accessor left the same hole in four other + /// paths, because the sentinel was a convention rather than a definition. + /// A renderer can now only get this wrong by not asking. + #[must_use] + pub fn is_measured(&self) -> bool { + self.nanos_per_op.is_finite() && self.nanos_per_op > 0.0 + } + /// The spread across this configuration's repetitions, as a multiple. /// /// `1.00` would mean every repetition took the same time. A wide spread @@ -362,7 +383,7 @@ pub fn ratio_bounds(numerator: Run, denominator: Run) -> Option<(f64, f64)> { pub fn format_ratio_bounded(numerator: Option, denominator: Option) -> String { match (numerator, denominator) { (Some(numerator), Some(denominator)) - if numerator.nanos_per_op > 0.0 && denominator.nanos_per_op > 0.0 => + if numerator.is_measured() && denominator.is_measured() => { let point = numerator.nanos_per_op / denominator.nanos_per_op; match ratio_bounds(numerator, denominator) { @@ -374,6 +395,20 @@ pub fn format_ratio_bounded(numerator: Option, denominator: Option) -> } } +/// The column width the report allocates to a bounded ratio. +/// +/// Named here, beside the formatter, rather than written as a literal in the +/// report's format string. [`format_ratio_bounded`] emits a point estimate *and* +/// its interval -- `1.00x [1.00-1.00]` is 17 characters, not the 5 a bare +/// `1.00x` would take -- and a Rust width is a minimum rather than a maximum, so +/// a field narrower than the value does not truncate it, it pushes every later +/// column out of line with its header. The report had been allocating 10. +/// +/// `ratio_column_is_wide_enough_for_its_formatter` holds the two together, so +/// widening the formatter's output without widening this fails a test rather +/// than silently skewing a table. +pub const RATIO_COLUMN_WIDTH: usize = 20; + /// Renders a scaling factor together with the interval it could occupy. /// /// See [`Observation::scaling_bounds`]: the bracketed interval is a bound over @@ -381,7 +416,11 @@ pub fn format_ratio_bounded(numerator: Option, denominator: Option) -> #[must_use] pub fn format_scaling_bounded(point: Option, bounds: Option<(f64, f64)>) -> String { match (point, bounds) { - (Some(point), _) if !point.is_finite() => "--".to_owned(), + // A scaling of zero is the sentinel, not a measurement: it means the + // many-producer row reported no throughput at all. Guarded here as well + // as against non-finite values, because zero is the half that renders + // plausibly -- `0.00x` looks like a contended queue, `infx` does not. + (Some(point), _) if !point.is_finite() || point <= 0.0 => "--".to_owned(), (Some(point), Some((low, high))) if low.is_finite() && high.is_finite() => { format!("{point:.2}x [{low:.2}-{high:.2}]") } @@ -404,19 +443,35 @@ pub fn render_table(out: &mut dyn fmt::Write, runs: &[Run]) { "shape", "producers", "ns/op", "ops/sec", "refusals", "ns/op range", "spread" ); for run in runs { + // `shape` and `producers` are configuration and always mean something. + // Every other column is a measurement, so a row that did not run has + // nothing to put in any of them -- including `refusals`, whose zero + // would otherwise read as "nothing was refused" rather than "nothing + // was attempted". See `Run::is_measured`. + let (nanos, ops, refusals, range, spread) = if run.is_measured() { + ( + format!("{:.1}", run.nanos_per_op), + format!("{:.0}", run.ops_per_second), + run.refusals.to_string(), + format!( + "{:.1}-{:.1}", + run.fastest_nanos_per_op, run.slowest_nanos_per_op + ), + format_scaling(run.spread()), + ) + } else { + ( + "--".to_owned(), + "--".to_owned(), + "--".to_owned(), + "--".to_owned(), + "--".to_owned(), + ) + }; let _ = writeln!( out, - "{:<18} {:>10} {:>14.1} {:>16.0} {:>14} {:>18} {:>9}", - run.shape, - run.producers, - run.nanos_per_op, - run.ops_per_second, - run.refusals, - format!( - "{:.1}-{:.1}", - run.fastest_nanos_per_op, run.slowest_nanos_per_op - ), - format_scaling(run.spread()), + "{:<18} {:>10} {:>14} {:>16} {:>14} {:>18} {:>9}", + run.shape, run.producers, nanos, ops, refusals, range, spread, ); } } @@ -432,7 +487,7 @@ pub fn render_table(out: &mut dyn fmt::Write, runs: &[Run]) { #[must_use] pub fn format_scaling(scaling: Option) -> String { match scaling { - Some(value) if value.is_finite() => format!("{value:.2}x"), + Some(value) if value.is_finite() && value > 0.0 => format!("{value:.2}x"), _ => "--".to_owned(), } } @@ -447,7 +502,7 @@ pub fn format_scaling(scaling: Option) -> String { pub fn format_ratio(numerator: Option, denominator: Option) -> String { match (numerator, denominator) { (Some(numerator), Some(denominator)) - if numerator.nanos_per_op > 0.0 && denominator.nanos_per_op > 0.0 => + if numerator.is_measured() && denominator.is_measured() => { format!("{:.2}x", numerator.nanos_per_op / denominator.nanos_per_op) } @@ -455,10 +510,18 @@ pub fn format_ratio(numerator: Option, denominator: Option) -> String } } -/// One row's nanoseconds per operation, or `--` when the row is missing. +/// One row's nanoseconds per operation, or `--` when the row is missing or did +/// not run. +/// +/// Guards the sentinel for the reason [`Run::is_measured`] records: this column +/// is the report's most-read number, and `0.0` in it reads as a shape too fast +/// to time rather than as one that never ran. #[must_use] pub fn format_nanos(run: Option) -> String { - run.map_or_else(|| "--".to_owned(), |run| format!("{:.1}", run.nanos_per_op)) + match run { + Some(run) if run.is_measured() => format!("{:.1}", run.nanos_per_op), + _ => "--".to_owned(), + } } /// Time every configuration. diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index f188953d5..ec9726996 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -924,3 +924,160 @@ fn render_table_marks_an_unmeasured_spread_rather_than_printing_zero() { "an unmeasured spread must be marked: {row:?}" ); } + +/// The sentinel is one predicate now, so it is pinned directly. +/// +/// [`Run::is_measured`] exists so a renderer cannot forget the test by writing +/// it slightly differently. That only helps if the predicate is itself right, +/// which a test routed through a formatter would not establish. +#[test] +fn is_measured_rejects_every_shape_of_unmeasured_row() { + assert!(run(shapes::RESERVING_MPSC, 1, 1e9).is_measured()); + + let mut row = run(shapes::RESERVING_MPSC, 1, 0.0); + assert!( + !row.is_measured(), + "a zero cost is the did-not-run sentinel" + ); + + row.nanos_per_op = f64::NAN; + assert!(!row.is_measured(), "NaN is not a measurement"); + + row.nanos_per_op = f64::INFINITY; + assert!(!row.is_measured(), "infinity is not a measurement"); + + row.nanos_per_op = -1.0; + assert!(!row.is_measured(), "a negative cost is not a measurement"); +} + +/// A row that did not run must not put a number in *any* measured column. +/// +/// The test above this one checked the spread cell alone, and passed while the +/// very same row published `0.0` ns/op, `0` ops/sec and a `0.0-0.0` range -- +/// three cells that read as a shape too fast to time. Checking one cell of a +/// row is what let the other four drift, so this asserts over the whole row. +#[test] +fn render_table_marks_every_measured_cell_of_a_row_that_did_not_run() { + let mut absent = run(shapes::SLOTWISE_MPSC, 8, 0.0); + absent.fastest_nanos_per_op = 0.0; + absent.slowest_nanos_per_op = 0.0; + let mut out = String::new(); + render_table(&mut out, &[absent]); + let row = out.lines().nth(1).expect("one row was rendered"); + + // Shape and producer count are configuration, not measurement: they are + // known whether or not the row ran, and must survive. + assert!( + row.contains(shapes::SLOTWISE_MPSC), + "the shape is configuration and must still be named: {row:?}" + ); + assert!( + row.contains('8'), + "the producer count is configuration and must survive: {row:?}" + ); + + assert_eq!( + row.matches("--").count(), + 5, + "ns/op, ops/sec, refusals, range and spread must all be marked: {row:?}" + ); + assert!( + !row.contains("0.0"), + "an unmeasured row published a number: {row:?}" + ); +} + +/// A shape that did not run must not publish a cost. +#[test] +fn format_nanos_marks_a_row_that_did_not_run() { + assert_eq!(format_nanos(Some(run(shapes::SLOTWISE_MPSC, 8, 0.0))), "--"); + + let mut broken = run(shapes::SLOTWISE_MPSC, 8, 1e9); + broken.nanos_per_op = f64::NAN; + assert_eq!(format_nanos(Some(broken)), "--"); +} + +/// Zero scaling is the sentinel, and it is the half that renders plausibly. +/// +/// [`Observation::scaling`] divides the many-producer rate by the one-producer +/// rate, so a many-producer row that did not run yields exactly `Some(0.0)`. +/// Rendered, that is `0.00x` in a column where values near `1` are the normal +/// reading -- it looks like a queue that failed to *scale* rather than one that +/// failed to *run*. The other sentinel, `infx`, at least announces itself. +#[test] +fn format_scaling_marks_a_zero_rather_than_publishing_it() { + assert_eq!(format_scaling(Some(0.0)), "--"); + assert_eq!(format_scaling_bounded(Some(0.0), None), "--"); + assert_eq!(format_scaling_bounded(Some(0.0), Some((0.0, 0.0))), "--"); +} + +/// The whole path, so the guard is pinned where a reader would meet it. +/// +/// The formatter tests above supply the sentinel by hand. This one makes the +/// probe's own arithmetic produce it, which is the only way to show the two +/// halves agree about what a did-not-run row looks like. +#[test] +fn a_many_producer_row_that_did_not_run_scales_to_the_marker() { + let observation = Observation { + isolated: vec![ + run(shapes::RESERVING_MPSC, 1, 1e8), + run(shapes::RESERVING_MPSC, 8, 0.0), + ], + drained: Vec::new(), + available_parallelism: Some(8), + }; + let point = observation.scaling(&observation.isolated, shapes::RESERVING_MPSC, 8); + assert_eq!( + point, + Some(0.0), + "the sentinel reaches the renderer as a plain zero" + ); + let bounds = observation.scaling_bounds(&observation.isolated, shapes::RESERVING_MPSC, 8); + assert_eq!( + format_scaling_bounded(point, bounds), + "--", + "a shape that never ran must not publish a scaling factor" + ); +} + +/// The layout table's ratio column must fit what its formatter emits. +/// +/// A Rust width is a *minimum*, so a value wider than its field is not +/// truncated -- it pushes every column after it out of alignment with its +/// header, silently. The layout table allocated 10 characters to a formatter +/// whose ordinary output is 17, so the second and third ratio columns had been +/// rendering seven and fourteen characters adrift. +/// +/// This binds the width to the formatter rather than restating it: widening +/// `format_ratio_bounded`'s output without widening `RATIO_COLUMN_WIDTH` fails +/// here instead of skewing a published table. +#[test] +fn ratio_column_is_wide_enough_for_its_formatter() { + // Both halves of the point-and-interval shape, plus the marker. + let rendered = [ + format_ratio_bounded( + Some(run_spanning(shapes::CLAIM_WIDE, 32, 40.0, 50.0, 60.0)), + Some(run_spanning(shapes::CLAIM_NARROW, 32, 4.0, 5.0, 6.0)), + ), + format_ratio_bounded( + Some(run_spanning(shapes::CLAIM_DEEP, 1, 9.9, 10.0, 10.1)), + Some(run_spanning(shapes::CLAIM_NARROW, 1, 9.9, 10.0, 10.1)), + ), + format_ratio_bounded(None, None), + ]; + for cell in &rendered { + assert!( + cell.len() <= RATIO_COLUMN_WIDTH, + "{cell:?} is {} characters and would push the next column out of \ + line with its header, which allows {RATIO_COLUMN_WIDTH}", + cell.len() + ); + } + // And the guard is only meaningful if the formatter really does emit the + // wide form here -- otherwise this would pass against a bare point estimate. + assert!( + rendered[0].contains('['), + "expected a bounded ratio, got {:?}", + rendered[0] + ); +} From eb807ca57db1f7304ccef4ffa8933fac9530ad69 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 23:13:07 -0700 Subject: [PATCH 063/139] docs(queues): stop calling a finite recurrence horizon an absolute one Every claim-position layout recurs. `Wide` moves the horizon to 2^64 pushes rather than removing it, which the `Wide` rustdoc already said outright -- "Longer, not unbounded" -- while three other sites still stated the withdrawn claim in words that shared no keyword with it: - D-41's index row: "it buys a guarantee rather than a lifetime argument", which is exactly what a longer horizon is not. - `Perpetual`: 20 years "puts the recurrence beyond any real deployment rather than merely far away" -- an absolute claim about the *shortest* of the deferred horizons, and one a caller pushing an order of magnitude faster falsifies in two years. - `permit_mpsc`: a wider field "moves the recurrence out of reach", immediately before claiming the shape "addresses the structure rather than the interval" -- which "out of reach" contradicts, since it is an interval claim. The earlier sweep that corrected "unreachable" and "no deployment reaches" missed all three because it swept the phrasings that had been written rather than the proposition they encode. Swept the proposition this time: "Wide/Perpetual is qualitatively different" across both crates. 4 sites found, 4 corrected; the `Wide` rustdoc and the probe's horizon table already agreed. The fourth site is the probe report's "8/56 merely deferring it", which lives in the probes crate and so landed in the preceding commit to keep the release scope clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/src/permit_mpsc.rs | 8 ++++---- crates/windows-waitable-queues/src/reserving_mpsc.rs | 6 ++++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index fdd05e81a..c11cf3979 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts sit outside the same-code control but too close to it to establish an ordering or a cost on this host: a flag to measure locally, not a finding. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). Which half the bits go to is the trade this decision exposes rather than settles: a 32-bit reservation field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, against a position whose width sets the recurrence horizon. The bound that matters on the reservation side is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. Which of the two a deployment needs is the deployment's question. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a guarantee rather than a lifetime argument. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts sit outside the same-code control but too close to it to establish an ordering or a cost on this host: a flag to measure locally, not a finding. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). Which half the bits go to is the trade this decision exposes rather than settles: a 32-bit reservation field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, against a position whose width sets the recurrence horizon. The bound that matters on the reservation side is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. Which of the two a deployment needs is the deployment's question. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a longer lifetime argument -- a recurrence at 2^64 rather than at 2^56 -- and not a different kind of argument. Like every horizon in that column it scales with the caller's push rate rather than being absolute. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/src/permit_mpsc.rs b/crates/windows-waitable-queues/src/permit_mpsc.rs index 3f01ca07c..92c705663 100644 --- a/crates/windows-waitable-queues/src/permit_mpsc.rs +++ b/crates/windows-waitable-queues/src/permit_mpsc.rs @@ -26,10 +26,10 @@ //! //! How wide that field is, and so how many pushes recurrence takes, is a layout //! choice there -- 32 bits under the default and up to 64 under -//! [`reserving_mpsc::Wide`](crate::reserving_mpsc). **That moves the recurrence -//! out of reach without removing the separation that causes it**, which is why -//! this shape remains interesting: it addresses the structure rather than the -//! interval. +//! [`reserving_mpsc::Wide`](crate::reserving_mpsc). **That widens the interval +//! before recurrence without removing the separation that causes it**, which is +//! why this shape remains interesting: it addresses the structure rather than +//! the interval. //! //! Here the decision *is* the operation. A producer takes a permit from a count //! of unspoken-for slots with one atomic, and that single modification both diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 2d518c3d2..2f29fda05 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -598,8 +598,10 @@ impl ClaimLayout for Enduring { /// Its field holds at most 255 outstanding reservations -- reachable only when /// capacity is at least that large, since the achievable count is the lesser of /// the two -- and its position recurs after 2^56 pushes, about -/// **20 years** at the pre-correction planning rate ([`ClaimLayout`]), which puts the recurrence -/// beyond any real deployment rather than merely far away. +/// **20 years** at the pre-correction planning rate ([`ClaimLayout`]). That is a +/// longer horizon, not the absence of one, and like every figure in that column +/// it scales with the caller's rate: a deployment pushing an order of magnitude +/// faster reaches it in about two. /// /// 255 reservations is the whole of the trade, and it is a real limit rather /// than a nominal one: [`Producer::reserve`] returns `None` once that many are From eee03aa1da06e347bcd8b34cad0ae2459d3d3565 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Tue, 15 Sep 2026 23:33:56 -0700 Subject: [PATCH 064/139] docs(probes): correct the M4.5 archive, which inverted its own lesson The archived M4.5 entry described `spread()` as returning "zero rather than infinity when a shape failed to run -- the same guard `format_ratio` carries for the same reason". Both halves are false as shipped, and the sentence is the discredited behaviour presented as the delivered feature. Ordering the branch's own commits: `fecd352` added `spread()` returning `0.0`; `bd82bf9` archived M4.5 with that description; `28978ba` then established that `0.0` renders as `0.00x`, reads as perfect stability, and was itself the defect -- changing `spread()` to return `Option`. `format_ratio` no longer carries a zero-returning guard either; it routes through `Run::is_measured`. The archive was not revisited, so it shipped teaching that returning zero was the fix. That is premise-removed/conclusion-kept, arising wholly inside this branch, in the one document written to be read later for *why* a thing was done. The intermediate state never existed on main and never will, so correcting it before merge records the only state that ships rather than rewriting landed history. The archive is append-only, so the correction is appended rather than substituted: the original wording is named in the note, because it did not merely go stale, it inverted the lesson. Swept the proposition across the crate and the root design notes before committing: one site stated it, this one. The code, the tests and the `spread()` rustdoc all already said `None`. Nothing detected the drift, which is what `M2.13` (lint the completed checklist archive mechanically in CI) is already queued to fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/COMPLETED-CHECKLIST.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 2cc2f99eb..f4fd43bdc 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1134,9 +1134,16 @@ figure to carry "the number of runs with their dispersion", and says a ratio quo of the figures that decision governs and did not satisfy it. Reported by review, and correctly. `Run` gained `fastest_nanos_per_op` and `slowest_nanos_per_op`, taken from the ends of the sort that -already existed, plus a `spread()` accessor that returns zero rather than infinity when a shape -failed to run -- the same guard `format_ratio` carries for the same reason. `render_table` publishes -an `ns/op range` column and a `spread` column. +already existed, plus a `spread()` accessor returning `Option` -- `None` for a shape that did +not run. `render_table` publishes an `ns/op range` column and a `spread` column. + +*(Corrected before merge. This item first shipped `spread()` returning `0.0` for the unmeasurable +case, recorded here as "the same guard `format_ratio` carries for the same reason". A later review +on the same branch found that `0.0` renders as `0.00x`, which reads as perfect stability -- the most +reassuring cell the column can hold, produced by a row that measured nothing. `spread()` now returns +`Option`, and every renderer routes through `Run::is_measured` rather than restating the test. The +original wording is noted rather than quietly replaced because it did not merely go stale: it +presented returning zero as the fix, when returning zero was the defect.)* Nine tests cover it, verified load-bearing by sabotage: taking the fastest from the median index instead of the minimum fails `median_run_carries_the_fastest_and_slowest_repetitions`. From 117a7adf236695f14570e08d1e9c32730ddd3293 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 02:37:41 -0700 Subject: [PATCH 065/139] fix(probes): count measured layouts, and size the ratio column from its cells Two renderer defects, both cases of a guard that stops one step short of where the value is actually used. `layouts_measured` asked whether a row was *present*, not whether it ran. A row can be present and carry the did-not-run sentinel, so the report could state "4 apportionments ... measured" directly above a table in which `render_table` marked one of them `--` in every measured cell. The count is now `Observation::count_measured`, which asks `Run::is_measured` and is testable, rather than a `find(..).is_some()` in the binary that no test could reach. `RATIO_COLUMN_WIDTH` was treated as a bound when it is only a floor. The interval endpoints `format_ratio_bounded` prints come from measured spans, so the cell's width is a function of data: a slow repetition against a fast one renders `10.00x [6.67-1500.00]`, 21 characters, which a width of 20 does not truncate -- it pushes the next two columns out of line with their headers, silently. Outliers of that size are ordinary on a loaded or virtualized host, and surviving them is why `median_run` takes a median at all. The width is now derived per table by `ratio_column_width` from the cells it must hold, with the constant as a floor so a narrow table still looks right. That requires rendering every cell before emitting the header, which is the only ordering that can get it right. The previous test asserted `cell.len() <= RATIO_COLUMN_WIDTH` and passed only because its fixtures were narrow -- it pinned an invariant that was never available. It now pins the derivation, and carries an assertion that the overrunning fixture still overruns, so the case cannot stop being exercised without failing. Verified by sabotage: reverting `count_measured` to `.is_some()` and `ratio_column_width` to the bare constant fails exactly the two tests written for them, and nothing else. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 85 ++++++++------ .../src/queue_contention.rs | 52 ++++++++- .../src/queue_contention/tests.rs | 104 +++++++++++++----- 3 files changed, 179 insertions(+), 62 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index e0bf81f4d..792c44f6e 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -13,8 +13,9 @@ //! cannot separate. use windows_platform_probes::queue_contention::{ - DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, RATIO_COLUMN_WIDTH, REPETITIONS, - format_nanos, format_ratio_bounded, format_scaling_bounded, measure, render_table, shapes, + DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, format_nanos, + format_ratio_bounded, format_scaling_bounded, measure, ratio_column_width, render_table, + shapes, }; use windows_platform_probes::report::emit_report; @@ -267,22 +268,22 @@ fn render(out: &mut dyn std::fmt::Write) { // Question 3: what does the claim word's apportionment and width cost? let _ = writeln!(out, "\n 3. claim-word layout\n"); - // Counted from what was actually measured rather than written as a literal: + // Counted from what was actually MEASURED rather than from what is present: // the 64/64 rows are cfg-elided on a target with no native 128-bit exchange, - // and a hardcoded "four" would be false there. - let layouts_measured = [ - shapes::CLAIM_NARROW, - shapes::CLAIM_DEEP, - shapes::CLAIM_PERPETUAL, - shapes::CLAIM_WIDE, - ] - .iter() - .filter(|shape| { - observation - .find(&observation.isolated, shape, PRODUCER_COUNTS[0]) - .is_some() - }) - .count(); + // and a hardcoded "four" would be false there -- but a row can also be + // present while carrying the did-not-run sentinel, which `render_table` + // marks `--` and this sentence would otherwise still count. See + // `Observation::count_measured`. + let layouts_measured = observation.count_measured( + &observation.isolated, + &[ + shapes::CLAIM_NARROW, + shapes::CLAIM_DEEP, + shapes::CLAIM_PERPETUAL, + shapes::CLAIM_WIDE, + ], + PRODUCER_COUNTS[0], + ); let _ = writeln!( out, " {layouts_measured} apportionments of reserving_mpsc's claim word, measured on" @@ -353,6 +354,39 @@ fn render(out: &mut dyn std::fmt::Write) { ("drained", &observation.drained), ] { let _ = writeln!(out, " -- {label} --"); + // Every cell is rendered before the header is emitted, because the ratio + // column's width is derived from the widest value it must hold. A Rust + // width is a minimum, so sizing the header first and discovering a wider + // cell later does not truncate that cell -- it silently pushes the two + // columns after it out of line. See `ratio_column_width`. + let rows: Vec<(usize, [String; 4], [String; 3])> = PRODUCER_COUNTS + .iter() + .map(|&producers| { + let narrow = observation.find(regime, shapes::CLAIM_NARROW, producers); + let deep = observation.find(regime, shapes::CLAIM_DEEP, producers); + let perpetual = observation.find(regime, shapes::CLAIM_PERPETUAL, producers); + let wide = observation.find(regime, shapes::CLAIM_WIDE, producers); + ( + producers, + [ + format_nanos(narrow), + format_nanos(deep), + format_nanos(perpetual), + format_nanos(wide), + ], + [ + format_ratio_bounded(deep, narrow), + format_ratio_bounded(perpetual, narrow), + format_ratio_bounded(wide, narrow), + ], + ) + }) + .collect(); + let w = ratio_column_width( + rows.iter() + .flat_map(|(_, _, ratios)| ratios) + .map(String::as_str), + ); let _ = writeln!( out, " {:<10} {:>11} {:>11} {:>11} {:>11} {:>w$} {:>w$} {:>w$}", @@ -364,25 +398,12 @@ fn render(out: &mut dyn std::fmt::Write) { "16/48 vs", "8/56 vs", "64/64 vs", - w = RATIO_COLUMN_WIDTH ); - for &producers in PRODUCER_COUNTS { - let narrow = observation.find(regime, shapes::CLAIM_NARROW, producers); - let deep = observation.find(regime, shapes::CLAIM_DEEP, producers); - let perpetual = observation.find(regime, shapes::CLAIM_PERPETUAL, producers); - let wide = observation.find(regime, shapes::CLAIM_WIDE, producers); + for (producers, nanos, ratios) in &rows { let _ = writeln!( out, " {:<10} {:>11} {:>11} {:>11} {:>11} {:>w$} {:>w$} {:>w$}", - producers, - format_nanos(narrow), - format_nanos(deep), - format_nanos(perpetual), - format_nanos(wide), - format_ratio_bounded(deep, narrow), - format_ratio_bounded(perpetual, narrow), - format_ratio_bounded(wide, narrow), - w = RATIO_COLUMN_WIDTH + producers, nanos[0], nanos[1], nanos[2], nanos[3], ratios[0], ratios[1], ratios[2], ); } let _ = writeln!(out); diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index d4abff5c9..3d44a7be1 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -279,6 +279,29 @@ impl Observation { .copied() } + /// How many of `shapes` actually produced a measurement at `producers`. + /// + /// Counted rather than written as a literal because the 64/64 rows are + /// `cfg`-elided on a target with no native 128-bit exchange, so a hardcoded + /// count would be false there. + /// + /// **Presence is not measurement.** A row can be present and still carry the + /// did-not-run sentinel, and the report's prose is where that distinction + /// escapes: `render_table` marks such a row `--` in every measured cell while + /// a sentence above it counts the shape as measured. Asking + /// [`Run::is_measured`] here is what keeps the two halves of the report + /// telling the same story. + #[must_use] + pub fn count_measured(&self, regime: &[Run], shapes: &[&str], producers: usize) -> usize { + shapes + .iter() + .filter(|shape| { + self.find(regime, shape, producers) + .is_some_and(|run| run.is_measured()) + }) + .count() + } + /// How far throughput scaled from one producer to `producers`. /// /// 1.0 means N producers together push no faster than one did, which is @@ -395,7 +418,7 @@ pub fn format_ratio_bounded(numerator: Option, denominator: Option) -> } } -/// The column width the report allocates to a bounded ratio. +/// The **minimum** width the report gives a bounded ratio column. /// /// Named here, beside the formatter, rather than written as a literal in the /// report's format string. [`format_ratio_bounded`] emits a point estimate *and* @@ -404,11 +427,32 @@ pub fn format_ratio_bounded(numerator: Option, denominator: Option) -> /// a field narrower than the value does not truncate it, it pushes every later /// column out of line with its header. The report had been allocating 10. /// -/// `ratio_column_is_wide_enough_for_its_formatter` holds the two together, so -/// widening the formatter's output without widening this fails a test rather -/// than silently skewing a table. +/// **This is a floor, not a bound, because the formatter has no bound.** The +/// interval's endpoints come from measured spans, so a slow outlier -- an +/// ordinary event on a loaded or virtualized host, and the reason `median_run` +/// takes a median at all -- widens the cell without limit: a 300 ms repetition +/// against a 4 ns one renders `10.00x [6.67-1500.00]`, which is 21. Use +/// [`ratio_column_width`] to size the column against the values it must actually +/// hold; this constant only stops a table of narrow values from looking cramped. pub const RATIO_COLUMN_WIDTH: usize = 20; +/// The width a ratio column must take to keep its rows aligned with its header. +/// +/// The widest cell the column has to hold, or [`RATIO_COLUMN_WIDTH`] when that +/// is wider. Derived from the rendered cells rather than assumed, because +/// [`format_ratio_bounded`]'s output length is a function of measured data and +/// therefore has no compile-time bound -- see [`RATIO_COLUMN_WIDTH`] for the +/// case that overruns it. A caller must render every cell of the column before +/// emitting the header, which is the only ordering that can get this right. +#[must_use] +pub fn ratio_column_width<'a>(cells: impl IntoIterator) -> usize { + cells + .into_iter() + .map(str::len) + .max() + .map_or(RATIO_COLUMN_WIDTH, |widest| widest.max(RATIO_COLUMN_WIDTH)) +} + /// Renders a scaling factor together with the interval it could occupy. /// /// See [`Observation::scaling_bounds`]: the bracketed interval is a bound over diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index ec9726996..d10a51e79 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -1040,7 +1040,7 @@ fn a_many_producer_row_that_did_not_run_scales_to_the_marker() { ); } -/// The layout table's ratio column must fit what its formatter emits. +/// The ratio column must fit every cell it is asked to hold. /// /// A Rust width is a *minimum*, so a value wider than its field is not /// truncated -- it pushes every column after it out of alignment with its @@ -1048,36 +1048,88 @@ fn a_many_producer_row_that_did_not_run_scales_to_the_marker() { /// whose ordinary output is 17, so the second and third ratio columns had been /// rendering seven and fourteen characters adrift. /// -/// This binds the width to the formatter rather than restating it: widening -/// `format_ratio_bounded`'s output without widening `RATIO_COLUMN_WIDTH` fails -/// here instead of skewing a published table. +/// **An earlier version of this test asserted `cell.len() <= RATIO_COLUMN_WIDTH`, +/// and that invariant is not available.** The interval's endpoints are measured +/// spans, so the cell's width is a function of data and has no compile-time +/// bound; the test passed only because its fixtures happened to be narrow. The +/// width is now derived from the cells, and what is pinned is that derivation. #[test] -fn ratio_column_is_wide_enough_for_its_formatter() { - // Both halves of the point-and-interval shape, plus the marker. - let rendered = [ - format_ratio_bounded( - Some(run_spanning(shapes::CLAIM_WIDE, 32, 40.0, 50.0, 60.0)), - Some(run_spanning(shapes::CLAIM_NARROW, 32, 4.0, 5.0, 6.0)), - ), - format_ratio_bounded( - Some(run_spanning(shapes::CLAIM_DEEP, 1, 9.9, 10.0, 10.1)), - Some(run_spanning(shapes::CLAIM_NARROW, 1, 9.9, 10.0, 10.1)), - ), - format_ratio_bounded(None, None), - ]; - for cell in &rendered { +fn ratio_column_fits_every_cell_it_must_hold() { + let ordinary = format_ratio_bounded( + Some(run_spanning(shapes::CLAIM_WIDE, 32, 40.0, 50.0, 60.0)), + Some(run_spanning(shapes::CLAIM_NARROW, 32, 4.0, 5.0, 6.0)), + ); + // The guard is only meaningful if the formatter really does emit the wide + // point-and-interval form -- otherwise it would pass against a bare `1.00x`. + assert!( + ordinary.contains('['), + "expected a bounded ratio, got {ordinary:?}" + ); + + // A slow outlier -- a 300 ms repetition against a 4 ns one, which is exactly + // what `median_run` takes a median to survive -- overruns the floor. + let outlier = format_ratio_bounded( + Some(run_spanning(shapes::CLAIM_WIDE, 32, 40.0, 50.0, 6000.0)), + Some(run_spanning(shapes::CLAIM_NARROW, 32, 4.0, 5.0, 6.0)), + ); + assert!( + outlier.len() > RATIO_COLUMN_WIDTH, + "this fixture exists to exceed the floor; if it no longer does, the \ + case it guards has stopped being exercised: {outlier:?}" + ); + + let cells = [ordinary.as_str(), outlier.as_str(), "--"]; + let width = ratio_column_width(cells); + for cell in cells { assert!( - cell.len() <= RATIO_COLUMN_WIDTH, + cell.len() <= width, "{cell:?} is {} characters and would push the next column out of \ - line with its header, which allows {RATIO_COLUMN_WIDTH}", + line with its header, which allows {width}", cell.len() ); } - // And the guard is only meaningful if the formatter really does emit the - // wide form here -- otherwise this would pass against a bare point estimate. - assert!( - rendered[0].contains('['), - "expected a bounded ratio, got {:?}", - rendered[0] +} + +/// The floor applies when every cell is narrower than it. +#[test] +fn ratio_column_never_narrows_below_its_floor() { + assert_eq!(ratio_column_width(["1.00x", "--"]), RATIO_COLUMN_WIDTH); + assert_eq!( + ratio_column_width(std::iter::empty()), + RATIO_COLUMN_WIDTH, + "a regime with no rows still needs a header that lines up" + ); +} + +/// A row that is present but never ran must not be counted as measured. +/// +/// The report says "N apportionments ... measured" above a table in which +/// `render_table` marks every cell of an unmeasured row `--`. Counting presence +/// rather than measurement let those two halves disagree: the sentence claimed +/// four layouts while the table showed one of them as having produced nothing. +#[test] +fn count_measured_counts_rows_that_ran_rather_than_rows_that_exist() { + let layouts = [ + shapes::CLAIM_NARROW, + shapes::CLAIM_DEEP, + shapes::CLAIM_PERPETUAL, + shapes::CLAIM_WIDE, + ]; + let observation = Observation { + isolated: vec![ + run(shapes::CLAIM_NARROW, 1, 1e8), + run(shapes::CLAIM_DEEP, 1, 1e8), + // Present, but carrying the did-not-run sentinel. + run(shapes::CLAIM_PERPETUAL, 1, 0.0), + // CLAIM_WIDE absent entirely, as it is when cfg-elided. + ], + drained: Vec::new(), + available_parallelism: Some(8), + }; + assert_eq!( + observation.count_measured(&observation.isolated, &layouts, 1), + 2, + "a present-but-unmeasured row must not be counted, and an absent one \ + must not be either" ); } From 0510b927dbdfef5acc9863a7ece3031ed2a0da51 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 02:38:00 -0700 Subject: [PATCH 066/139] docs: state every claim-position horizon as finite and rate-dependent Third sweep of the same proposition, and the third to find sites the previous one missed -- because each swept the phrasings already written rather than the claim they encode. These three share no keyword with "unreachable" or "no deployment reaches", which is what the earlier sweeps grepped for: - The probe's own horizon table gives 8/56 twenty years and 64/64 5,039 at the same rate, while the paragraph above it said 8/56 "reaches the same practical headroom a 128-bit word gives". They differ by a factor of 250. - The same section called 12/52 "the first row that is not" reachable. The table gives it 202 days at the conservative floor, which a long-lived process reaches comfortably. - `Balanced`'s rustdoc said `Wide` moves the recurrence to 2^64 pushes "rather than to a horizon in years", contradicting `Wide`'s own rustdoc twenty lines below, which says 2^64 is about 5,000 years and calls it "Longer, not unbounded". Every layout recurs. What changes down the column is how long it takes and at what rate, so the prose now says that and leaves the durations to the table that computes them. Worth noting where these survived: the table is correct and always was. It was the prose around it drawing conclusions the table contradicts -- the argument for a figure having one home, made against itself. Swept across both crates: 3 sites found, 3 corrected. The remaining "unreachable" hits are a different proposition (a capacity ceiling of 2^31 slots, which is tens of gigabytes) or describe prior art's claims rather than ours, and are left alone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/DESIGN-NOTES.md | 14 +++++++++----- .../windows-waitable-queues/src/reserving_mpsc.rs | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 6bfec90b1..2d7de1976 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -917,9 +917,11 @@ reservations, which is a field ceiling rather than a reachable count: on target, 2^30 on a 32-bit one), and a smaller queue binds it sooner still. Narrowing the field is what buys the position bits: 2^21 reservations leaves about a -day, 2^12 leaves over a year, and 2^8 leaves twenty years. The last reaches the -same practical headroom a 128-bit word gives, on a plain `AtomicU64`, without a -third-party dependency and without reopening `D-18`'s i686 question. +day, 2^12 leaves over a year, and 2^8 leaves twenty years. That last is a plain +`AtomicU64`, so it reaches twenty years without a third-party dependency and +without reopening `D-18`'s i686 question -- against the 128-bit word's 5,039 at +the same rate. Both are finite and both scale with the caller's rate; which of +them is enough is a question about a deployment, not one this table answers. **An earlier version of this paragraph called the reservation half "the half worth least", on the premise that outstanding reservations are bounded by how @@ -943,8 +945,10 @@ statement about what the re-apportionment costs to run. See [Re-measured on the shipping type](#d-queue-layout-observations). So the arithmetic separates the rows this way: 16/48's 12.7 days at the -conservative floor is still reachable by a busy long-lived process, and 12/52 is -the first row that is not. Which of them a caller wants is the caller's question, +conservative floor is reachable by a busy long-lived process within a single +uptime, and 12/52's 202 days is reachable within a long one. Every row recurs; +what changes down the column is how long it takes, and at what rate. Which of +them a caller wants is the caller's question, and the shipping type takes the layout as a parameter so it stays theirs -- see [D-no-client-prescriptions](#d-no-client-prescriptions). diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 2f29fda05..affdafb7e 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -566,7 +566,8 @@ impl ClaimWord for u128 { /// [`Enduring`] holds up to 65,535 outstanding reservations, [`Perpetual`] up to /// 255 -- each reachable only when the queue's capacity is at least that /// large -- -/// and `Wide` moves it to 2^64 pushes rather than to a horizon in years. (`Wide` exists +/// and `Wide` moves it to 2^64 pushes -- a longer horizon in years, not the absence of +/// one. (`Wide` exists /// only under the `dwcas` feature, so this names it without linking: an /// intra-doc link here would not resolve in a default-feature rustdoc build.) #[derive(Debug, Clone, Copy, PartialEq, Eq)] From 65e74e1ed97e743d6e814ce96ea31b610db97afa Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 09:01:35 -0700 Subject: [PATCH 067/139] docs: present the data and stop, rather than drawing the reader's conclusion Records as a repository rule what `D-no-client-prescriptions` already said for the probes crate, and fixes the one site in this branch that still violated it after the previous round claimed to have fixed it. The previous round treated "8/56 reaches the same practical headroom a 128-bit word gives" as restatement drift and proposed a mechanical answer. That diagnosis was wrong. Nothing there is inconsistent -- the table says twenty years and 5,039 years, and the prose simply decides, on the reader's behalf, that a factor of 250 does not matter to them. No checker can find that, because there is nothing to check against. It is editorializing, and the decision forbidding it was already on file. The repair is to delete the conclusion, not to correct it. The previous round did correct it: "12/52 is the first row that is not reachable" became "reachable by a busy long-lived process within a single uptime", which is the same move with the verdict flipped, and is equally a claim about a deployment nobody here has measured. It now reads "every row recurs; what changes down the column is how long that takes at a given rate -- 16/48 at 12.7 days against 12/52 at 202 days". The rule carries a deliberate carve-out. `// effectively unreachable` beside the line it describes is ordinary idiom, read by someone already in that code, and it hedges itself. The rule bites on `.md` prose, which is read out of context by people deciding whether to adopt something. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 30 +++++++++++++++++++ .../windows-platform-probes/DESIGN-NOTES.md | 7 ++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d6792d13d..c1de55b0a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1299,6 +1299,36 @@ sites in three wordings. ask. This says the same of measurements: hold the number once, and have prose point rather than paraphrase. +### 5. In prose we own, present the data and stop; do not draw the conclusion + +Rule 4 governs where a number lives. This governs whether you state what it *means*. In any `.md` +prose this repository owns, give the reader the figures and the mechanism, and leave the verdict to +them — because the verdict is almost always a claim about *their* deployment, which we have not +measured and do not know. + +The failure does not look like an error, which is why it survives review. It reads as helpfulness: + +- A table gives 8/56 twenty years and 64/64 5,039 years at the same rate; the paragraph above it + says 8/56 "reaches the same practical headroom a 128-bit word gives." Nothing is inconsistent — + the prose has simply decided, on the reader's behalf, that a factor of 250 does not matter to + them. +- A table gives 12/52 202 days; the prose calls it "the first row that is not reachable." +- **Flipping the verdict is not the fix.** Replacing "not reachable" with "reachable by a busy + long-lived process" is the same move with the opposite conclusion. The repair is to delete the + conclusion, not to correct it: *"every row recurs; what changes down the column is how long that + takes at a given rate — 16/48 at 12.7 days against 12/52 at 202 days."* + +**A code comment is different, and the difference is deliberate.** `// effectively unreachable` or +`// unreachable in all practicality` beside the line it describes is ordinary English idiom, read by +someone already working on that code, and it carries its own hedge. The rule bites on `.md` prose, +which is read by people deciding whether to adopt something, out of context, long after. + +This is **not a new rule** — it is [D-no-client-prescriptions](../crates/windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions) +("state what was observed … stop there"), stated once for the repository rather than once for the +crate that happens to publish measurements. Every instance found so far has been a violation of that +existing decision rather than a gap in it, so reach for the decision before proposing a mechanism: +no checker can find these, because nothing is inconsistent. + ## REVIEW FEEDBACK — answer it where it was raised, not only in the commit **A review round is not finished when the code changes. It is finished when the reviewer has diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 2d7de1976..017b39749 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -944,10 +944,9 @@ and a rate, not from a measurement of either layout; what does not follow is any statement about what the re-apportionment costs to run. See [Re-measured on the shipping type](#d-queue-layout-observations). -So the arithmetic separates the rows this way: 16/48's 12.7 days at the -conservative floor is reachable by a busy long-lived process within a single -uptime, and 12/52's 202 days is reachable within a long one. Every row recurs; -what changes down the column is how long it takes, and at what rate. Which of +So the arithmetic separates the rows this way: every row recurs, and what changes +down the column is how long that takes at a given rate -- 16/48 at 12.7 days +against 12/52 at 202 days, at the conservative floor. Which of them a caller wants is the caller's question, and the shipping type takes the layout as a parameter so it stays theirs -- see [D-no-client-prescriptions](#d-no-client-prescriptions). From fd0864f182d99264f4709e94ed2886e6aba645ad Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 09:07:12 -0700 Subject: [PATCH 068/139] docs: make rule 5 absolute, removing the carve-out that invited argument The rule as first written had three places to argue: "almost always" a claim about the reader, a carve-out exempting code comments, and a scope that "bites on `.md` prose". Each is a judgment a reviewer has to make before deciding whether a sentence violates the rule, which is how a rule produces nuanced findings instead of clear ones. The carve-out is the worst of the three, because its boundary cannot be drawn. Rustdoc is a code comment AND published prose that people read while deciding whether to adopt something -- the case the rule most needs to cover, and the one the exemption made undecidable. So: no exception for context, and no hedges. Give the figures and the mechanism, and stop. Added the words that usually mark the failure (practical, effectively, reachable, enough, negligible, safe to, and any sentence whose subject is the reader) so recognising a violation does not depend on taste. Verified the branch against the stricter rule: the four `unreachable` and `practical` uses in its Rust are either structural claims about code paths or descriptions of claims this branch withdrew, and none writes a conclusion for the reader. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c1de55b0a..0489375fc 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1299,12 +1299,18 @@ sites in three wordings. ask. This says the same of measurements: hold the number once, and have prose point rather than paraphrase. -### 5. In prose we own, present the data and stop; do not draw the conclusion +### 5. Present what was observed; never write the conclusion -Rule 4 governs where a number lives. This governs whether you state what it *means*. In any `.md` -prose this repository owns, give the reader the figures and the mechanism, and leave the verdict to -them — because the verdict is almost always a claim about *their* deployment, which we have not -measured and do not know. +Rule 4 governs where a number lives. This governs whether you state what it *means*. Give the +figures and the mechanism. Stop. Do not tell the reader what follows for them. + +**This holds everywhere, and has no exception for context.** An earlier draft exempted code +comments, reasoning that `// effectively unreachable` is ordinary idiom read by someone already in +that code. That exemption is withdrawn, because its boundary cannot be drawn: rustdoc is a code +comment *and* published prose that people read while deciding whether to adopt something, which is +the case the rule most needs to cover. A rule with an undecidable boundary gets re-argued at every +review, and the argument costs more than the rule saves. There is no setting in which writing the +reader's conclusion for them is wanted, so there is nothing to except. The failure does not look like an error, which is why it survives review. It reads as helpfulness: @@ -1314,20 +1320,19 @@ The failure does not look like an error, which is why it survives review. It rea them. - A table gives 12/52 202 days; the prose calls it "the first row that is not reachable." - **Flipping the verdict is not the fix.** Replacing "not reachable" with "reachable by a busy - long-lived process" is the same move with the opposite conclusion. The repair is to delete the - conclusion, not to correct it: *"every row recurs; what changes down the column is how long that - takes at a given rate — 16/48 at 12.7 days against 12/52 at 202 days."* + long-lived process" is the same move with the opposite conclusion. Delete the conclusion, do not + correct it: *"every row recurs; what changes down the column is how long that takes at a given + rate — 16/48 at 12.7 days against 12/52 at 202 days."* -**A code comment is different, and the difference is deliberate.** `// effectively unreachable` or -`// unreachable in all practicality` beside the line it describes is ordinary English idiom, read by -someone already working on that code, and it carries its own hedge. The rule bites on `.md` prose, -which is read by people deciding whether to adopt something, out of context, long after. +Three words are the usual tell, and each is a conclusion wearing a measurement's clothes: +**practical**, **effectively**, **reachable**. So are "enough", "negligible", "safe to", and any +sentence whose subject is the reader. This is **not a new rule** — it is [D-no-client-prescriptions](../crates/windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions) ("state what was observed … stop there"), stated once for the repository rather than once for the crate that happens to publish measurements. Every instance found so far has been a violation of that -existing decision rather than a gap in it, so reach for the decision before proposing a mechanism: -no checker can find these, because nothing is inconsistent. +existing decision rather than a gap in it. Apply it while writing: no checker can find these, +because nothing is inconsistent. ## REVIEW FEEDBACK — answer it where it was raised, not only in the commit From 401733b796ec80b201477c4853fee95efda4d5ad Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 09:11:40 -0700 Subject: [PATCH 069/139] docs: draw rule 5's boundary at the slash count, which is where it is The previous commit removed the code-comment carve-out, arguing its boundary could not be drawn because rustdoc is both a code comment and published prose. That argument was wrong, and so was the fix. Rustdoc is not a code comment. It is the `.md` colocated with the code -- the same published prose, read by the same people, deciding whether to adopt the thing. A `//` comment is a different artifact with a different job: carrying the developer's mind-state to whoever edits that line next, including what was uncertain and what was traded. `// effectively unreachable` there is not a lapse to be tolerated; conveying a judgment, hedges and all, is what the comment is for, and flattening it into a bare figure would destroy what it was written to preserve. So the rule governs `.md`, `///` and `//!`, and does not govern `//`. That is a syntactic test, which is what removes the nuance -- not banning nuance everywhere, but putting the boundary somewhere no taste is needed to find it. Two drafts got this wrong in opposite directions; the note in the rule records both so the next reader does not re-derive either. Re-checked the branch against the corrected scope: the four flagged uses are all rustdoc, and all are either structural claims about code paths or descriptions of claims this branch withdrew. The diff adds no plain `//` comment of this kind, so the exemption changes nothing here -- it is stated for the next author, not to excuse anything present. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0489375fc..cc9aa834f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1304,13 +1304,20 @@ paraphrase. Rule 4 governs where a number lives. This governs whether you state what it *means*. Give the figures and the mechanism. Stop. Do not tell the reader what follows for them. -**This holds everywhere, and has no exception for context.** An earlier draft exempted code -comments, reasoning that `// effectively unreachable` is ordinary idiom read by someone already in -that code. That exemption is withdrawn, because its boundary cannot be drawn: rustdoc is a code -comment *and* published prose that people read while deciding whether to adopt something, which is -the case the rule most needs to cover. A rule with an undecidable boundary gets re-argued at every -review, and the argument costs more than the rule saves. There is no setting in which writing the -reader's conclusion for them is wanted, so there is nothing to except. +**Where it applies is syntactic, so there is nothing to adjudicate.** It governs `.md` files and +rustdoc — `///` and `//!`. It does not govern an ordinary `//` comment. + +That split is about purpose, not about how public the text happens to be. **Rustdoc is not a code +comment; it is the `.md` colocated with the code** — the same published prose, read by the same +people, deciding whether to adopt the thing. A `//` comment exists to carry the *developer's +mind-state* to whoever edits that line next: why this was done this way, what was uncertain, what +was traded. `// effectively unreachable` there is not a lapse to be tolerated — **conveying a +judgment, hedges and all, is that comment's whole job**, and flattening it into a bare figure would +destroy the thing it was written to preserve. + +An earlier draft of this rule got the boundary wrong twice: first exempting "code comments" as a +single category, then withdrawing the exemption on the grounds that rustdoc made the boundary +undecidable. It is decidable, and it is the slash count. The failure does not look like an error, which is why it survives review. It reads as helpfulness: @@ -1326,7 +1333,14 @@ The failure does not look like an error, which is why it survives review. It rea Three words are the usual tell, and each is a conclusion wearing a measurement's clothes: **practical**, **effectively**, **reachable**. So are "enough", "negligible", "safe to", and any -sentence whose subject is the reader. +sentence whose subject is the reader. In `.md` and rustdoc they mark a violation; in a `//` comment +they mark a developer saying what they thought, which is wanted. + +This is **not a new rule** — it is [D-no-client-prescriptions](../crates/windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions) +("state what was observed … stop there"), stated once for the repository rather than once for the +crate that happens to publish measurements. Every instance found so far has been a violation of that +existing decision rather than a gap in it. Apply it while writing: no checker can find these, +because nothing is inconsistent. This is **not a new rule** — it is [D-no-client-prescriptions](../crates/windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions) ("state what was observed … stop there"), stated once for the repository rather than once for the From ae12bec62abafd1c88b87e7e33ededc65d8dac5a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 09:31:12 -0700 Subject: [PATCH 070/139] docs: remove a paragraph rule 5 said twice The closing paragraph of CONTRACT INTEGRITY rule 5 appeared verbatim twice. Introduced two commits ago: the replacement text ended with that paragraph while the text it replaced stopped short of it, so the original survived and a second copy was appended below it. A careless edit boundary rather than a drafting error -- but it landed in the rule about not restating things, which is the sort of coincidence worth writing down rather than quietly fixing. Found by review; no duplicate-detection ran over the file, and none would have, because nothing in the gate reads prose. Swept the file for other duplicated long lines while here: none. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cc9aa834f..c69a3ba91 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1342,12 +1342,6 @@ crate that happens to publish measurements. Every instance found so far has been existing decision rather than a gap in it. Apply it while writing: no checker can find these, because nothing is inconsistent. -This is **not a new rule** — it is [D-no-client-prescriptions](../crates/windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions) -("state what was observed … stop there"), stated once for the repository rather than once for the -crate that happens to publish measurements. Every instance found so far has been a violation of that -existing decision rather than a gap in it. Apply it while writing: no checker can find these, -because nothing is inconsistent. - ## REVIEW FEEDBACK — answer it where it was raised, not only in the commit **A review round is not finished when the code changes. It is finished when the reviewer has From b95dda8a54c87b2ead0165a7b42929473d9192af Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 09:43:21 -0700 Subject: [PATCH 071/139] fix(probes): reject non-finite spans, and size the last two ratio tables Two findings from Copilot's PR review, both real. **A `<= 0.0` test does not reject `NaN`.** Every IEEE comparison against NaN is false, so all four disjuncts of `ratio_bounds`' guard were false for a NaN endpoint and it fell through to the division, rendering `[NaN-NaN]`. `Run::spread` had the mirror of it: `f64::INFINITY > 0.0` is true, and a finite slowest over an infinite fastest is `0.0` -- a spread of zero, the reassuring end of that column, from a row carrying nonsense. Both now test `is_finite()`, which excludes NaN and the infinities together. The fields are public, so neither needed `median_run` to be reachable. (The review's stated consequence for `spread` -- that `format_scaling` publishes `0.00x` -- is no longer true, because that formatter gained a `> 0.0` guard earlier in this branch. The input validation was still missing, which is what this fixes.) **The width derivation reached one table of three.** The isolated-scaling and drained-comparison tables still hard-coded 22 for cells that `format_scaling_bounded` and `format_ratio_bounded` size from measured data. Both now pre-render and call `ratio_column_width`, as the claim-layout table already did. That is this branch's third instance of a fix applied one call site short, and the second on this exact defect -- the previous round widened the layout table and left its two neighbours, having named that class in its own commit message. While there: the derivation now includes the header labels rather than relying on the floor to exceed them, which closes the latent gap a reviewer flagged as not-yet-a-defect in the layout table. Also here, from the same review: two rustdoc references pinned `blob/main` on GitHub, which can resolve to a different revision than the crate being read. Both are now paths relative to the source file. Verified by sabotage: reverting either guard fails exactly the test written for it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 103 ++++++++++++------ .../src/queue_contention.rs | 31 ++++-- .../src/queue_contention/tests.rs | 65 +++++++++++ 3 files changed, 161 insertions(+), 38 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 792c44f6e..9374765ef 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -117,25 +117,46 @@ fn render(out: &mut dyn std::fmt::Write) { out, " 1. push-path scaling with producer count (isolated regime)\n" ); + // Same two-pass shape as the claim-layout table below, and for the same + // reason: `format_scaling_bounded` renders a point plus a measured interval, + // whose width follows the data and has no fixed maximum. See + // `ratio_column_width`. + let scaling_rows: Vec<(usize, [String; 4])> = PRODUCER_COUNTS + .iter() + .map(|&producers| { + let cell = |shape: &str| { + format_scaling_bounded( + observation.scaling(&observation.isolated, shape, producers), + observation.scaling_bounds(&observation.isolated, shape, producers), + ) + }; + ( + producers, + [ + cell(shapes::SLOTWISE_MPSC), + cell(shapes::RESERVING_MPSC), + cell(shapes::PERMIT_MPSC), + cell(shapes::BASELINE_FETCH_ADD), + ], + ) + }) + .collect(); + let w = ratio_column_width( + scaling_rows + .iter() + .flat_map(|(_, cells)| cells) + .map(String::as_str), + ); let _ = writeln!( out, - " {:<12} {:>22} {:>22} {:>22} {:>22}", + " {:<12} {:>w$} {:>w$} {:>w$} {:>w$}", "producers", "slotwise", "reserving", "permit", "atomic floor" ); - for &producers in PRODUCER_COUNTS { - let cell = |shape: &str| { - format_scaling_bounded( - observation.scaling(&observation.isolated, shape, producers), - observation.scaling_bounds(&observation.isolated, shape, producers), - ) - }; + for (producers, cells) in &scaling_rows { let _ = writeln!( out, - " {producers:<12} {:>22} {:>22} {:>22} {:>22}", - cell(shapes::SLOTWISE_MPSC), - cell(shapes::RESERVING_MPSC), - cell(shapes::PERMIT_MPSC), - cell(shapes::BASELINE_FETCH_ADD) + " {producers:<12} {:>w$} {:>w$} {:>w$} {:>w$}", + cells[0], cells[1], cells[2], cells[3], ); } let _ = writeln!( @@ -184,33 +205,52 @@ fn render(out: &mut dyn std::fmt::Write) { out, " ratio still does not isolate it, or bound it either way.\n" ); + // Two-pass again: both ratio columns hold `format_ratio_bounded` output, + // whose width follows the measured span. The header labels join the + // derivation rather than being assumed to fit, so the column is correct by + // construction instead of by the floor happening to exceed them. + let drained_rows: Vec<(usize, String, String, String, String, String)> = PRODUCER_COUNTS + .iter() + .map(|&producers| { + let plain = observation.find(&observation.drained, shapes::SLOTWISE_MPSC, producers); + let reserving = + observation.find(&observation.drained, shapes::RESERVING_MPSC, producers); + let permit = observation.find(&observation.drained, shapes::PERMIT_MPSC, producers); + ( + producers, + format_nanos(plain), + format_nanos(reserving), + format_ratio_bounded(reserving, plain), + format_nanos(permit), + // The column SH-15.5 exists to fill: the experimental claim + // against the shipping shape it would replace. Below 1.00 means + // the permit claim is cheaper; above means removing the + // room-decision race costs throughput. + format_ratio_bounded(permit, reserving), + ) + }) + .collect(); + let r = ratio_column_width( + drained_rows + .iter() + .flat_map(|(_, _, _, ratio, _, permit_ratio)| [ratio.as_str(), permit_ratio.as_str()]) + .chain(["reserving/slotwise", "permit/reserving", "ratio [bound]"]), + ); let _ = writeln!( out, - " {:<10} {:>12} {:>12} {:>22} {:>12} {:>22}", + " {:<10} {:>12} {:>12} {:>r$} {:>12} {:>r$}", "producers", "slotwise", "reserving", "reserving/slotwise", "permit", "permit/reserving" ); let _ = writeln!( out, - " {:<10} {:>12} {:>12} {:>22} {:>12} {:>22}", + " {:<10} {:>12} {:>12} {:>r$} {:>12} {:>r$}", "", "ns/op", "ns/op", "ratio [bound]", "ns/op", "ratio [bound]" ); - for &producers in PRODUCER_COUNTS { - let plain = observation.find(&observation.drained, shapes::SLOTWISE_MPSC, producers); - let reserving = observation.find(&observation.drained, shapes::RESERVING_MPSC, producers); - let permit = observation.find(&observation.drained, shapes::PERMIT_MPSC, producers); - let ratio = format_ratio_bounded(reserving, plain); - // The column SH-15.5 exists to fill: the experimental claim against the - // shipping shape it would replace. Below 1.00 means the permit claim is - // cheaper; above means removing the room-decision race costs throughput. - let permit_ratio = format_ratio_bounded(permit, reserving); + for (producers, plain, reserving, ratio, permit, permit_ratio) in &drained_rows { let _ = writeln!( out, - " {producers:<10} {:>12} {:>12} {:>22} {:>12} {:>22}", - format_nanos(plain), - format_nanos(reserving), - ratio, - format_nanos(permit), - permit_ratio + " {producers:<10} {:>12} {:>12} {:>r$} {:>12} {:>r$}", + plain, reserving, ratio, permit, permit_ratio, ); } let _ = writeln!( @@ -385,7 +425,8 @@ fn render(out: &mut dyn std::fmt::Write) { let w = ratio_column_width( rows.iter() .flat_map(|(_, _, ratios)| ratios) - .map(String::as_str), + .map(String::as_str) + .chain(["16/48 vs", "8/56 vs", "64/64 vs"]), ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 3d44a7be1..1e559e4c1 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -192,7 +192,7 @@ pub struct Run { /// about its own stability within a run, and discarding them silently was /// the crate publishing a figure its own contract forbids. /// - /// [`d-observations-not-verdicts`]: https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/DESIGN-NOTES.md#d-observations-not-verdicts + /// [`d-observations-not-verdicts`]: ../DESIGN-NOTES.md#d-observations-not-verdicts pub fastest_nanos_per_op: f64, /// Slowest of the [`REPETITIONS`] timed repetitions, in nanoseconds per /// operation. See [`Run::fastest_nanos_per_op`]. @@ -239,7 +239,14 @@ impl Run { /// the exception, and the exception is what a renderer got wrong. #[must_use] pub fn spread(&self) -> Option { - if self.fastest_nanos_per_op > 0.0 { + // `> 0.0` alone is not the test: `f64::INFINITY > 0.0` is true, and a + // finite slowest over an infinite fastest is `0.0` -- a spread of zero, + // which is the reassuring end of this column. Both endpoints must be + // real numbers before dividing them. + if self.fastest_nanos_per_op.is_finite() + && self.fastest_nanos_per_op > 0.0 + && self.slowest_nanos_per_op.is_finite() + { Some(self.slowest_nanos_per_op / self.fastest_nanos_per_op) } else { None @@ -337,7 +344,7 @@ impl Observation { /// it reports -- see `M4.4`, which asks for candidates to be interleaved /// with their controls. /// - /// [`d-observations-not-verdicts`]: https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/DESIGN-NOTES.md#d-observations-not-verdicts + /// [`d-observations-not-verdicts`]: ../DESIGN-NOTES.md#d-observations-not-verdicts #[must_use] pub fn scaling_bounds( &self, @@ -375,10 +382,20 @@ impl Observation { /// is reported rather than divided by. #[must_use] pub fn ratio_bounds(numerator: Run, denominator: Run) -> Option<(f64, f64)> { - if numerator.fastest_nanos_per_op <= 0.0 - || numerator.slowest_nanos_per_op <= 0.0 - || denominator.fastest_nanos_per_op <= 0.0 - || denominator.slowest_nanos_per_op <= 0.0 + // **A `<= 0.0` test does not reject `NaN`.** IEEE comparison against NaN is + // false whichever way it is written, so every one of these four disjuncts is + // false for a NaN endpoint and the guard falls through to the division, + // which then renders `[NaN-NaN]`. Testing `is_finite()` first is what + // actually excludes it, and it excludes infinities in the same move. + let endpoints = [ + numerator.fastest_nanos_per_op, + numerator.slowest_nanos_per_op, + denominator.fastest_nanos_per_op, + denominator.slowest_nanos_per_op, + ]; + if endpoints + .iter() + .any(|endpoint| !endpoint.is_finite() || *endpoint <= 0.0) { return None; } diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index d10a51e79..1b6fa3843 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -1133,3 +1133,68 @@ fn count_measured_counts_rows_that_ran_rather_than_rows_that_exist() { must not be either" ); } + +/// A `<= 0.0` test does not reject `NaN`, and this pins that it is rejected. +/// +/// Every comparison against `NaN` is false, so the four-way `<= 0.0` guard this +/// function used to carry fell straight through to the division for a `NaN` +/// endpoint and produced `[NaN-NaN]`. The endpoints are public fields, so this +/// is reachable without going through `median_run`. +#[test] +fn ratio_bounds_rejects_non_finite_endpoints() { + let sound = run_spanning(shapes::RESERVING_MPSC, 8, 4.0, 5.0, 6.0); + assert!( + ratio_bounds(sound, sound).is_some(), + "the fixture must otherwise produce a bound, or this proves nothing" + ); + + for poison in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + let mut numerator = sound; + numerator.fastest_nanos_per_op = poison; + assert_eq!( + ratio_bounds(numerator, sound), + None, + "a {poison} numerator span must not reach the division" + ); + + let mut denominator = sound; + denominator.slowest_nanos_per_op = poison; + assert_eq!( + ratio_bounds(sound, denominator), + None, + "a {poison} denominator span must not reach the division" + ); + } +} + +/// `> 0.0` admits infinity, and a finite slowest over it is a spread of zero. +/// +/// Zero is the reassuring end of the spread column, so this is the same class +/// of defect as the sentinel that reached the renderer earlier: a row that +/// measured nothing coherent reporting perfect stability. +#[test] +fn spread_rejects_non_finite_span_endpoints() { + let mut row = run(shapes::RESERVING_MPSC, 8, 1e8); + row.fastest_nanos_per_op = 10.0; + row.slowest_nanos_per_op = 13.0; + assert!(row.spread().is_some(), "the fixture must otherwise measure"); + + let mut infinite_fastest = row; + infinite_fastest.fastest_nanos_per_op = f64::INFINITY; + assert_eq!( + infinite_fastest.spread(), + None, + "an infinite fastest divides to a spread of zero, which reads as \ + perfect stability" + ); + + for poison in [f64::NAN, f64::INFINITY] { + let mut broken = row; + broken.slowest_nanos_per_op = poison; + assert_eq!(broken.spread(), None, "a {poison} slowest has no spread"); + + let mut broken = row; + broken.fastest_nanos_per_op = poison; + assert_eq!(broken.spread(), None, "a {poison} fastest has no spread"); + } +} From eef1544f62b5fafd586b219fa6d4d6ad08f2a8a5 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 09:44:05 -0700 Subject: [PATCH 072/139] docs: publish only the exposure figures the disclosed rate supports Copilot's review found three public documents stating 2^32 pushes as "37 seconds to roughly four minutes ... about two minutes at two producers", attributed to "this crate's disclosed rates". Those documents disclose one rate: about 116 million pushes per second. It gives 37 seconds. Four minutes needs about 18M/s and two minutes needs about 36M/s, and neither appears anywhere in the documents that cite them. The figures are residue: the rates they were computed from were replaced when the probe's timing window was corrected, and the conclusions stayed. Premise removed, conclusion kept, in three homes -- README, crate rustdoc, and the `reserving_mpsc` module header. Each now states the arithmetic its own document supports and stops. The substantive half of the withdrawn clause survives, because it was never about duration: two producers is the smallest count that can trigger the defect at all. Also in the root design notes, both found by the same review: - The prose-volume section published "it appears in five separate files" three lines above declaring "the exact counts are deliberately not recorded here". That is the section committing its own subject, for the second time -- the first was a table that drifted within days. Now qualitative. - The section claimed a formal-methods survey is queued in the root `CHECKLIST.md`. It is not: that work is M30 on an unmerged branch, so the reference dangles for anyone reading this one. Removed rather than repointed, since a committed note should not cite a branch. - Linked the `mutation-sweeps/2026-09-02/` capture directory, which the surrounding prose invites the reader to open. And in the probe, two rustdoc references pinned `blob/main` on GitHub, which can resolve to a different revision than the crate being read. Both are now relative paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 7 +++---- crates/windows-waitable-queues/README.md | 8 ++++---- crates/windows-waitable-queues/src/lib.rs | 8 ++++---- crates/windows-waitable-queues/src/reserving_mpsc.rs | 8 ++++---- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 1a1dd0804..7991d1016 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1879,7 +1879,7 @@ lines and markdown together. That ratio turns out to be the wrong thing to watch The thing to watch is that in `windows-waitable-queues`, a handful of single facts -- `Perpetual`'s reservation-count ceiling, `Balanced`'s recurrence horizon, `Perpetual`'s position span, `Balanced`'s field ceiling -- are each restated many times across several files, by hand, with nothing checking -any of them. The ceiling is the worst: it appears in five separate files. +any of them. The ceiling is the worst, restated in more places than any of the others. **The exact counts are deliberately not recorded here.** An earlier version of this section carried them as a table, and the table drifted within days: one row gained an occurrence when a qualifier was @@ -1981,7 +1981,7 @@ them wrong -- it said "in both cases roughly 60%" where one of the two cases was was adjacent and the summary of it was false, because prose is not checkable and nobody checks it. **This repository already contains the better pattern and did not apply it here.** -`mutation-sweeps/2026-09-02/` is a dated, committed capture directory: data as an artifact, cited +[`mutation-sweeps/2026-09-02/`](mutation-sweeps/2026-09-02) is a dated, committed capture directory: data as an artifact, cited rather than retyped. `windows-platform-probes`, which produces the most-cited numbers in the workspace, commits no capture at all -- every figure it has ever published reached its document by hand. @@ -2040,6 +2040,5 @@ they know.)* **No work is scheduled by this note.** It was written to inform a decision that has not been taken, and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule -rather than an oversight. A formal-methods survey is queued separately in the root -[CHECKLIST.md](CHECKLIST.md) and bears on the same question. If the table-versus-constants test or a +rather than an oversight. If the table-versus-constants test or a prose-reduction pass is adopted, each needs its own item at that time. diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index d025b465f..886e0ad6d 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -185,10 +185,10 @@ says so -- which is why this is documented here rather than left to a caller to discover, and why it cannot be mitigated after the fact. **The exposure, as arithmetic over a disclosed rate.** Under `Balanced`, 2^32 -pushes is 37 seconds to roughly four minutes of *sustained* pushing at this -crate's disclosed rates -- about two minutes at two producers, which is the -smallest count that can trigger it at all. **Those rates predate a correction to -the probe's timing window** and are kept as a floor for the reason the layout +pushes is about 37 seconds of *sustained* pushing at the rate the layout table +above discloses. Two producers is the smallest count that can trigger the defect +at all. **That rate predates a correction to +the probe's timing window** and is kept as a floor for the reason the layout table above gives: the correction lowers the rate and lengthens the horizon, so these figures say the wrap arrives sooner than it does, which is the conservative direction for a hazard. That is sustained throughput, not a total diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index a4cd07132..30e02745c 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -163,10 +163,10 @@ //! to a caller to discover, and why it cannot be mitigated after the fact. //! //! **The exposure, as arithmetic over a disclosed rate.** Under `Balanced`, 2^32 -//! pushes is 37 seconds to roughly four minutes of *sustained* pushing at this -//! crate's disclosed rates -- about two minutes at two producers, which is -//! the smallest count that can trigger it at all. **Those rates predate a -//! correction to the probe's timing window** and are kept as a floor for the +//! pushes is about 37 seconds of *sustained* pushing at the rate the layout +//! table above discloses. Two producers is the smallest count that can trigger +//! the defect at all. **That rate predates a +//! correction to the probe's timing window** and is kept as a floor for the //! reason the layout table above gives: the correction lowers the rate and //! lengthens the horizon, so these figures say the wrap arrives sooner than it //! does, which is the conservative direction for a hazard. That is sustained diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index affdafb7e..d03aaac44 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -20,10 +20,10 @@ //! silent**: the consumer receives a different item than was sent, and no error, //! panic, or counter reports it. //! -//! Under `Balanced`, 2^32 pushes is 37 seconds to about four minutes of -//! *sustained* pushing at this crate's disclosed rates, roughly two minutes at -//! two producers. Those rates predate a correction to the probe's timing window, -//! so they are a floor rather than a forecast -- the correction lowers the rate +//! Under `Balanced`, 2^32 pushes is about 37 seconds of +//! *sustained* pushing at the rate [`ClaimLayout`] discloses; two producers is +//! the smallest count that can trigger it. That rate predates a correction to the probe's timing window, +//! so it is a floor rather than a forecast -- the correction lowers the rate //! and lengthens the horizon, which is the conservative direction for a hazard; //! see [`ClaimLayout`]. The wrap alone is not enough -- a producer must also stall //! inside a window a few instructions wide -- but a preemption suffices. From c471adf4fdca9fe0e3cf35ef4e194000fc4505a8 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 09:47:49 -0700 Subject: [PATCH 073/139] fix(probes): stop the drained consumer when the producer phase unwinds Reported inline on PR #90, and correct. Each of the four drained timers clears its consumer's stop flag on the line after the producer scope returns. A producer's assertion -- which fires precisely when the consumer is already gone -- unwinds straight past that line, so the consumer keeps spinning on a flag nobody will set, and its `JoinHandle` is dropped without a join. The probe then leaves a thread burning a core for the life of the process, in exactly the run someone is trying to read a failure out of. `StopOnDrop` clears the flag from `Drop`, which runs on both paths. The explicit join stays on the success path, where its return value is the refusal count; on the unwind path the thread is detached but terminates promptly, which is the part that mattered. Applied to all four drained timers rather than the one the comment cited, since they share the shape -- and this branch has now shipped three defects that were a correct fix applied to one of several call sites. Verified by sabotage: emptying the `Drop` body fails the new test on the unwind path, which is the path that was broken; the success path passed either way. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/queue_contention.rs | 39 ++++++++++++++-- .../src/queue_contention/tests.rs | 46 +++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 1e559e4c1..2b6bfa9a7 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -914,10 +914,35 @@ fn time_isolated_permit(producers: usize) -> Repetition { /// backpressure the way a real one would. pub const DRAINED_CAPACITY: usize = 1024; +/// Stops a drained timer's consumer however the producer phase ends. +/// +/// Each drained timer parks a consumer in `while !done { ... }` and clears the +/// flag once the producer scope returns. On the **failure** path that line is +/// never reached: a producer's assertion unwinds straight past it, so the +/// consumer keeps spinning on a flag nobody will ever set, and its `JoinHandle` +/// is dropped without a join. The probe then leaves a thread burning a core for +/// the life of the process -- in exactly the run someone is trying to read a +/// failure out of. +/// +/// Clearing the flag from `Drop` runs on both paths, so the consumer observes +/// the stop and finishes whether the producers succeeded or panicked. The join +/// is still done explicitly on the success path, where its return value is the +/// refusal count; on the unwind path the thread is detached, but it terminates +/// promptly rather than spinning, which is the part that mattered. +struct StopOnDrop(Arc); + +impl Drop for StopOnDrop { + fn drop(&mut self) { + self.0.store(true, Ordering::Relaxed); + } +} + fn time_drained_mpsc(producers: usize) -> Repetition { let (tx, rx) = slotwise_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); let done = Arc::new(AtomicBool::new(false)); let consumer_done = Arc::clone(&done); + // Set on every exit path, not just the one that returns. See StopOnDrop. + let stop = StopOnDrop(done); // The consumer is a barrier participant, not merely spawned: spawning is not // readiness, and a consumer still in thread start-up while producers push // turns the opening of the run into an undrained regime. @@ -985,7 +1010,7 @@ fn time_drained_mpsc(producers: usize) -> Repetition { }); let elapsed = measured_span(&spans); - done.store(true, Ordering::Relaxed); + drop(stop); drop(tx); let refusals = consumer.join().expect("the consumer must not panic"); (elapsed, refusals) @@ -1015,6 +1040,8 @@ fn time_drained_reserving(producers: usize) -> Repetition { let (tx, rx) = reserving_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); let done = Arc::new(AtomicBool::new(false)); let consumer_done = Arc::clone(&done); + // Set on every exit path, not just the one that returns. See StopOnDrop. + let stop = StopOnDrop(done); // The consumer joins the gate here for the reason it does in the slotwise // twin: a run whose opening is undrained is not the regime being measured. let gate = start_barrier(producers + 1); @@ -1063,7 +1090,7 @@ fn time_drained_reserving(producers: usize) -> Repetition { }); let elapsed = measured_span(&spans); - done.store(true, Ordering::Relaxed); + drop(stop); drop(tx); let refusals = consumer.join().expect("the consumer must not panic"); (elapsed, refusals) @@ -1082,6 +1109,8 @@ fn time_drained_permit(producers: usize) -> Repetition { let (tx, rx) = permit_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); let done = Arc::new(AtomicBool::new(false)); let consumer_done = Arc::clone(&done); + // Set on every exit path, not just the one that returns. See StopOnDrop. + let stop = StopOnDrop(done); let gate = start_barrier(producers + 1); let consumer_gate = Arc::clone(&gate); @@ -1128,7 +1157,7 @@ fn time_drained_permit(producers: usize) -> Repetition { }); let elapsed = measured_span(&spans); - done.store(true, Ordering::Relaxed); + drop(stop); drop(tx); let refusals = consumer.join().expect("the consumer must not panic"); (elapsed, refusals) @@ -1193,6 +1222,8 @@ fn time_drained_layout(producers: usize) -> Repetition reserving_mpsc::bounded_as::(DRAINED_CAPACITY).expect("a valid capacity"); let done = Arc::new(AtomicBool::new(false)); let consumer_done = Arc::clone(&done); + // Set on every exit path, not just the one that returns. See StopOnDrop. + let stop = StopOnDrop(done); // The consumer joins the gate for the reason its twins do: a run whose // opening is undrained is not the regime being measured. let gate = start_barrier(producers + 1); @@ -1240,7 +1271,7 @@ fn time_drained_layout(producers: usize) -> Repetition .collect::>() }); let elapsed = measured_span(&spans); - done.store(true, Ordering::Relaxed); + drop(stop); let refusals = consumer.join().expect("the consumer must not panic"); (elapsed, refusals) } diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 1b6fa3843..b0a6e484d 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -1198,3 +1198,49 @@ fn spread_rejects_non_finite_span_endpoints() { assert_eq!(broken.spread(), None, "a {poison} fastest has no spread"); } } + +/// The stop flag must be set on the path where nobody sets it explicitly. +/// +/// The drained timers cleared the flag on the line after their producer scope, +/// which a producer's assertion unwinds straight past -- leaving the consumer +/// spinning on a flag nobody would ever set, its handle dropped unjoined, and a +/// core burning for the life of the process. The success path was never in +/// doubt; this pins the failure path, which is the one that was broken. +/// +/// Note this asserts the *observable* effect through an `Arc` the guard does not +/// own, rather than reading the guard back: a consumer sees the flag through +/// exactly such a clone. +#[test] +fn stop_on_drop_sets_the_flag_when_the_producer_phase_unwinds() { + let flag = Arc::new(AtomicBool::new(false)); + + // The success path, for contrast. + let observed = Arc::clone(&flag); + { + let _stop = StopOnDrop(Arc::clone(&flag)); + assert!( + !observed.load(Ordering::Relaxed), + "the flag must stay clear while the guard is alive, or a consumer \ + would stop before the producers had finished" + ); + } + assert!( + observed.load(Ordering::Relaxed), + "a normal drop must stop it" + ); + + // The path that was broken. `catch_unwind` prints the panic to stderr; no + // panic hook is installed to silence it, because a hook is process-global + // and this suite runs its tests as threads in one process. + let unwound = Arc::new(AtomicBool::new(false)); + let observed = Arc::clone(&unwound); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _stop = StopOnDrop(Arc::clone(&unwound)); + panic!("a producer failed, as one does when the consumer is gone"); + })); + assert!(result.is_err(), "the fixture must actually unwind"); + assert!( + observed.load(Ordering::Relaxed), + "the consumer would spin forever on a flag nobody sets" + ); +} From a1e7d455eb924836f66388e324578fb405b7c178 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 11:14:33 -0700 Subject: [PATCH 074/139] fix(probes): the one-producer scaling bound is exact, not an interval Reported by Copilot's review, and right. At one producer `scaling_bounds` looks up the same row twice and hands both spans to `ratio_bounds`, which treats one measurement as two independent ones. The result is an interval around a quantity that is 1.0 by construction -- uncertainty invented by the arithmetic rather than observed, printed in the report's first row, where it reads as measurement noise about a shape compared with itself. The bound was never unsound: it contains 1.0. What was wrong is that it was maximally loose at exactly the point where it is exactly known, and a reader cannot tell those apart from the rendering. A previous round of this branch cited that wide first row approvingly, as an illustration that these are bounds rather than sampled distributions. That reading was a rationalisation of a defect: the illustration is real, but it should not be paid for with a figure that misdescribes the one row whose value is certain. The general case keeps its real interval, which the new test asserts alongside the identity, so the special case cannot quietly swallow the function. Also links two plain-text `DESIGN-NOTES.md` references in public module docs, which rendered as unclickable text in the generated documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 3 +- .../src/queue_contention.rs | 16 ++++++- .../src/queue_contention/tests.rs | 46 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index 9374765ef..b3bf31dc1 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -4,7 +4,8 @@ //! //! **An experiment, not a component.** These probes measure platform behaviour //! and are not for production use. Do not call them from production code, and -//! do not lift a technique out of here. See this crate's DESIGN-NOTES.md. +//! do not lift a technique out of here. See this crate's +//! [DESIGN-NOTES.md](../../DESIGN-NOTES.md). //! //! This reports observations that bear on two questions otherwise settled by //! taste: whether the linked and sharded MPSC shapes are ever needed, and diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 2b6bfa9a7..0a72917e1 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -9,7 +9,8 @@ //! //! **An experiment, not a component.** These probes measure platform behaviour //! and are not for production use. Do not call them from production code, and -//! do not lift a technique out of here. See this crate's DESIGN-NOTES.md. +//! do not lift a technique out of here. See this crate's +//! [DESIGN-NOTES.md](../DESIGN-NOTES.md). //! //! # The two decisions this exists to force //! @@ -354,6 +355,19 @@ impl Observation { ) -> Option<(f64, f64)> { let one = self.find(regime, shape, 1)?; let many = self.find(regime, shape, producers)?; + if producers == 1 { + // **The one-producer row is a row divided by itself.** Its scaling is + // exactly 1.0 by construction, not approximately 1.0 by measurement, + // so there is no interval to report. Handing both spans to + // `ratio_bounds` would treat one measurement as two independent ones + // and manufacture a bound like `[0.84-1.20]` around a quantity that + // cannot be anything but 1 -- uncertainty invented by the arithmetic + // rather than observed, printed in the report's first row. + // + // The bound stays sound either way, since it contains 1.0. It is + // tightness that is at stake, and at the identity it is exact. + return Some((1.0, 1.0)); + } // Scaling is a RATE ratio -- many over one -- which is the COST ratio // one over many, so the rows go in that order. ratio_bounds(one, many) diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index b0a6e484d..801993808 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -1199,6 +1199,52 @@ fn spread_rejects_non_finite_span_endpoints() { } } +/// At one producer the bound is exact, because the row is divided by itself. +/// +/// The report's first row is this case. Passing both spans to `ratio_bounds` +/// treats one measurement as two independent ones and manufactures an interval +/// around a quantity that is 1 by construction -- uncertainty invented by the +/// arithmetic rather than observed, in the most prominent row on the page. +#[test] +fn scaling_bounds_at_one_producer_is_exactly_one() { + // A deliberately wide span: if the identity case were not special-cased, + // this row would publish a correspondingly wide bound. + let observation = Observation { + isolated: vec![run_spanning(shapes::RESERVING_MPSC, 1, 4.0, 5.0, 6.0)], + drained: Vec::new(), + available_parallelism: Some(8), + }; + assert_eq!( + observation.scaling(&observation.isolated, shapes::RESERVING_MPSC, 1), + Some(1.0), + "the point estimate is one by construction" + ); + assert_eq!( + observation.scaling_bounds(&observation.isolated, shapes::RESERVING_MPSC, 1), + Some((1.0, 1.0)), + "and so is the bound; a wider one would be invented, not measured" + ); + + // The general case must keep its real bound, or this special case has + // simply broken the function. + let observation = Observation { + isolated: vec![ + run_spanning(shapes::RESERVING_MPSC, 1, 4.0, 5.0, 6.0), + run_spanning(shapes::RESERVING_MPSC, 8, 40.0, 50.0, 60.0), + ], + drained: Vec::new(), + available_parallelism: Some(8), + }; + let (low, high) = observation + .scaling_bounds(&observation.isolated, shapes::RESERVING_MPSC, 8) + .expect("both rows present"); + assert!( + low < high, + "a genuine comparison of two rows still spans an interval, got \ + [{low}, {high}]" + ); +} + /// The stop flag must be set on the path where nobody sets it explicitly. /// /// The drained timers cleared the flag on the line after their producer scope, From 757d9430034a4cad9e56c31cf1f4964fcbdbeefd Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 11:14:56 -0700 Subject: [PATCH 075/139] docs: present the measured relations without deciding what they mean Copilot's review, applying the rule this branch added two days into its own prose. Each of these states a conclusion the data supports rather than the data: - The layout section said the figures were "a reasonable input for planning a deployment on comparable hardware and an unreasonable basis for a comparative claim". That is advice about how to use the data. - It then said the 128-bit rows sit "three to four times the 64-bit rows, an order of magnitude outside anything the same-code control does". Both multipliers depend on a baseline the sentence does not name -- read against the other u64 layouts rather than against 32/32, the first is about three; read against the control's ratio rather than its excess over unity, the second is about three rather than ten. It now states the relation the table can be checked against: the 128-bit rows fall outside the control band and the re-apportionments do not. - The queue README called the control range "wide enough to swallow small differences". Three corrections of fact from the same review: - `Perpetual`'s rustdoc said a ten-times-faster deployment "reaches it in about two" -- no unit. Two years. - `slotwise_mpsc` said it measured slower "on the hosts tried", but the README now carries one attributed host; the two-host capture was withdrawn. Singular. - `slotwise_mpsc` and `permit_mpsc` said "this crate's disclosed rates" predate the timing correction. The crate discloses two different things: a pre-correction planning rate used for recurrence arithmetic, and a post-correction measured table. Only the first predates it, so as written these mislabelled the current benchmark as stale. Both now name the planning rate, as `ClaimLayout` does. And two applications of the one-home rule to the rule's own file and to the rationale: the instructions' cautionary example computed "57 of 61 -- 93%", a proportion over counts it had just shown, which is the error the surrounding paragraph forbids; and `DESIGN-RATIONALE.md` restated the control's measured span instead of linking the capture. That span currently has seven homes across four files. They agree today; the remaining six are left as a deliberate scope call rather than an oversight, since consolidating them is a documentation sweep rather than part of this review round. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 2 +- crates/windows-platform-probes/DESIGN-NOTES.md | 8 +++----- crates/windows-platform-probes/DESIGN-RATIONALE.md | 6 ++++-- crates/windows-waitable-queues/README.md | 3 +-- crates/windows-waitable-queues/src/permit_mpsc.rs | 5 +++-- crates/windows-waitable-queues/src/reserving_mpsc.rs | 2 +- crates/windows-waitable-queues/src/slotwise_mpsc.rs | 8 +++++--- 7 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c69a3ba91..f723ef757 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1291,7 +1291,7 @@ sites in three wordings. document is not a finding; it is a hand-computed copy of one, checked by nobody and stale the moment any input moves. The counts are the finding. This rule was earned: an instructions file in this repository claimed "in both cases roughly 60%" about two figures given four words earlier, - one of which was 57 of 61 — 93%. + one of which was 57 of 61. - **The same applies to incidental tallies** — test counts, file counts, line counts. If the number is not itself the finding, leave it out; "the gate is green" says what "308 lib tests" pretends to. diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 017b39749..b17af814f 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1024,9 +1024,7 @@ instrument](#d-variance-is-a-finding) for what to try first and how to recognise the floor, and M4.2 in [CHECKLIST.md](CHECKLIST.md) for the probe controls that make those steps executable without a source edit. -What follows is therefore reported as *data with a known-unexplained spread*, -which is a reasonable input for planning a deployment on comparable hardware and -an unreasonable basis for a comparative claim about the layouts. +What follows is therefore reported as *data with a known-unexplained spread*. **Isolated regime**, median of the per-run ratios with the observed range beside it. The drained regime is reported in the paragraph below the table, and mixing @@ -1046,8 +1044,8 @@ In the drained regime nothing separates at all -- every u64 layout *and* the median is 1.13x at one producer, against a control that reaches 1.27x). **Widening the word is the one effect this probe establishes.** At sixteen and -thirty-two producers the isolated 128-bit rows sit three to four times the -64-bit rows, an order of magnitude outside anything the same-code control does. +thirty-two producers the isolated 128-bit rows fall outside the same-code +control band; the `u64` re-apportionments do not, at any producer count. That is a real effect on this machine, and its direction is mechanically unsurprising -- `cmpxchg16b` against `lock cmpxchg`. Whether it reproduces on another microarchitecture is a question for the probe, not for this note. diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 3b1a5f518..2c728be90 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -555,10 +555,12 @@ adjusting it. What made the repair possible was already in the probe's output. `reserving_mpsc` and `reserving(32/32)` are the same code at the same layout, measured twice per run, so their ratio is an *empirical* answer to "what does no difference look -like here" -- 0.68-1.27x across seven runs. That is a control the instrument +like here". That is a control the instrument derives rather than a floor the prose asserts, which is [D-derived-not-restated](DESIGN-NOTES.md#d-derived-not-restated) applied to a -measurement instead of to a fact. +measurement instead of to a fact. Its measured span is in the control table in +[DESIGN-NOTES.md](DESIGN-NOTES.md#d-variance-is-a-finding), which is where it is +recorded rather than here. **The tempting repair was to invert the claim**, since the seven-run medians put the re-apportionments at 1.23-1.30x at high producer counts. That would have been diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 886e0ad6d..55d1beb6a 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -451,8 +451,7 @@ this host's 8 physical cores, and the spread is not small at either scale. 226.5 over a 181.5-242.3 range across its five repetitions. The parenthesised ranges in the table above are the wider quantity: the extremes over all fifteen repetitions of the three captured runs. The probe's same-code -control has been measured at 0.68-1.27x over seven runs, which is wide enough to -swallow small differences; see +control has been measured at 0.68-1.27x over seven runs; see [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). That seven-run sweep is a **separate capture** taken to size the noise floor, not a longer version of this table -- its medians differ from the ones above, which is diff --git a/crates/windows-waitable-queues/src/permit_mpsc.rs b/crates/windows-waitable-queues/src/permit_mpsc.rs index 92c705663..d4385168f 100644 --- a/crates/windows-waitable-queues/src/permit_mpsc.rs +++ b/crates/windows-waitable-queues/src/permit_mpsc.rs @@ -78,8 +78,9 @@ use crate::metrics::Metrics; /// /// **64 bits on every target, deliberately, rather than `usize`**, for the same /// reason [`slotwise_mpsc`](crate::slotwise_mpsc) made the same choice: a -/// 32-bit counter laps in minutes at this crate's disclosed rates (a floor, -/// since they predate a timing correction that lowers them), and a shape +/// 32-bit counter laps in minutes at the pre-correction planning rate +/// [`reserving_mpsc::ClaimLayout`] documents (a floor, since that rate +/// overstates throughput), and a shape /// whose soundness depends on the target's pointer width is not one this crate /// ships twice over. /// diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index d03aaac44..c74f42c2c 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -602,7 +602,7 @@ impl ClaimLayout for Enduring { /// **20 years** at the pre-correction planning rate ([`ClaimLayout`]). That is a /// longer horizon, not the absence of one, and like every figure in that column /// it scales with the caller's rate: a deployment pushing an order of magnitude -/// faster reaches it in about two. +/// faster reaches it in about two years. /// /// 255 reservations is the whole of the trade, and it is a real limit rather /// than a nominal one: [`Producer::reserve`] returns `None` once that many are diff --git a/crates/windows-waitable-queues/src/slotwise_mpsc.rs b/crates/windows-waitable-queues/src/slotwise_mpsc.rs index a465acc59..9d45e87b0 100644 --- a/crates/windows-waitable-queues/src/slotwise_mpsc.rs +++ b/crates/windows-waitable-queues/src/slotwise_mpsc.rs @@ -84,8 +84,9 @@ use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; /// counter cannot lap. /// /// With `usize` it can. On a 32-bit target the counter laps after 2^32 claims, -/// which at this crate's disclosed rates is a matter of minutes -- a floor, -/// since those rates predate a timing correction that lowers them: the stalled +/// which at the pre-correction planning rate [`reserving_mpsc::ClaimLayout`] +/// documents is a matter of minutes -- a floor, since that rate overstates +/// throughput: the stalled /// producer then sees the same tail bits, succeeds, and writes a slot that has /// since been refilled from the previous lap of the ring. Every other guard in /// this shape holds -- the position really is claimed by exactly one producer; @@ -194,7 +195,8 @@ pub fn bounded(capacity: usize) -> Result<(Producer, Consumer), Capacit /// /// That avoidance is what distinguishes the two multi-producer shapes, but /// **it is not what makes either one faster**: measurement found this shape the -/// slower of the two under contention on the hosts tried. See the crate +/// slower of the two under contention on the host the crate's table was taken +/// on. See the crate /// documentation's attributed table for the figures and the conditions they were /// taken under. (An earlier version of this sentence gave "by up to 6.4x", a /// figure from a two-host capture withdrawn for predating a correction to the From ccac1ec1bb860f0718fa1627a87a816b88635dd7 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 11:30:47 -0700 Subject: [PATCH 076/139] fix(probes): a bound needs a measurement, and the guard needs all four spans Two findings from review, both against code the previous two rounds added. **The one-producer identity bound skipped the sentinel check.** Last round made `scaling_bounds` return an exact `(1.0, 1.0)` at one producer, because that case divides a row by itself. But `find` returns a row whether or not it ran, so a present-but-unmeasured row got a bound reporting perfect certainty about a shape that measured nothing. The point estimate goes `NaN` there and the current renderer suppresses it, which is why the report did not show it -- but this is a public method, and a caller reading the bound alone would see a certainty that is not there. Both rows are now asked `is_measured` before the identity case. That is the sentinel class landing inside the fix for a different defect, which is the second time on this branch that a correction has introduced one. **The non-finite guard's test constrained half of it.** `ratio_bounds` reads four span endpoints; the test written for it last round poisoned two -- `numerator.fastest` and `denominator.slowest`, the pair feeding the LOWER bound. Dropping either upper-bound endpoint from the guard left the test green while the function returned a `NaN` high. It now poisons all four independently, with `NaN` and both infinities, and asserts the renderer prints no bracket built from a poisoned endpoint -- the point estimate is still publishable there, since the medians are sound; the interval is not. A test weaker than its name, in the test written to close that exact hole. Fourth on this branch, third written by one of these rounds' own fixes. Verified by sabotage: dropping the `is_measured` check fails the extended identity test, and narrowing the endpoint array back to two fails the extended guard test naming `denominator.fastest` -- one of the two the old test never touched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/queue_contention.rs | 9 +++ .../src/queue_contention/tests.rs | 80 +++++++++++++++---- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 0a72917e1..17214e0f8 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -355,6 +355,15 @@ impl Observation { ) -> Option<(f64, f64)> { let one = self.find(regime, shape, 1)?; let many = self.find(regime, shape, producers)?; + // Asked before the identity case below, not after: a row that did not + // run is still a row, so `find` returns it and `producers == 1` would + // otherwise hand back an exact `(1.0, 1.0)` for a shape that measured + // nothing. The point estimate goes `NaN` in that case and the current + // renderer suppresses it, but this is a public method and a caller + // reading the bound on its own would see a certainty that is not there. + if !one.is_measured() || !many.is_measured() { + return None; + } if producers == 1 { // **The one-producer row is a row divided by itself.** Its scaling is // exactly 1.0 by construction, not approximately 1.0 by measurement, diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 801993808..5365a7df4 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -1148,22 +1148,54 @@ fn ratio_bounds_rejects_non_finite_endpoints() { "the fixture must otherwise produce a bound, or this proves nothing" ); + // **All four endpoints, each on its own.** An earlier version of this test + // poisoned `numerator.fastest` and `denominator.slowest` only -- the two + // that feed the LOWER bound -- so dropping either of the upper bound's + // endpoints from the guard would have left it green while `ratio_bounds` + // returned a `NaN` high. + /// A named span endpoint, so each can be poisoned independently. + type Endpoint = (&'static str, fn(&mut Run, f64)); + let fields: [Endpoint; 2] = [ + ("fastest", |run, value| run.fastest_nanos_per_op = value), + ("slowest", |run, value| run.slowest_nanos_per_op = value), + ]; for poison in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { - let mut numerator = sound; - numerator.fastest_nanos_per_op = poison; - assert_eq!( - ratio_bounds(numerator, sound), - None, - "a {poison} numerator span must not reach the division" - ); - - let mut denominator = sound; - denominator.slowest_nanos_per_op = poison; - assert_eq!( - ratio_bounds(sound, denominator), - None, - "a {poison} denominator span must not reach the division" - ); + for (field, set) in fields { + let mut numerator = sound; + set(&mut numerator, poison); + assert_eq!( + ratio_bounds(numerator, sound), + None, + "numerator.{field} = {poison} must not reach the division" + ); + + let mut denominator = sound; + set(&mut denominator, poison); + assert_eq!( + ratio_bounds(sound, denominator), + None, + "denominator.{field} = {poison} must not reach the division" + ); + + // The medians are still sound, so the point estimate is publishable + // and the *bound* is not. What must never appear is a bracket built + // from a poisoned endpoint. + for (label, rendered) in [ + ( + "numerator", + format_ratio_bounded(Some(numerator), Some(sound)), + ), + ( + "denominator", + format_ratio_bounded(Some(sound), Some(denominator)), + ), + ] { + assert!( + !rendered.contains('['), + "{label}.{field} = {poison} rendered an interval: {rendered:?}" + ); + } + } } } @@ -1243,6 +1275,24 @@ fn scaling_bounds_at_one_producer_is_exactly_one() { "a genuine comparison of two rows still spans an interval, got \ [{low}, {high}]" ); + + // A present-but-unmeasured one-producer row must not get the exact bound. + // `find` returns it, so the identity case would otherwise report perfect + // certainty about a shape that measured nothing. + for absent in [0.0, f64::NAN, f64::INFINITY] { + let mut row = run_spanning(shapes::RESERVING_MPSC, 1, 4.0, 5.0, 6.0); + row.nanos_per_op = absent; + let observation = Observation { + isolated: vec![row], + drained: Vec::new(), + available_parallelism: Some(8), + }; + assert_eq!( + observation.scaling_bounds(&observation.isolated, shapes::RESERVING_MPSC, 1), + None, + "a row reporting {absent} has no scaling to bound" + ); + } } /// The stop flag must be set on the path where nobody sets it explicitly. From b1acb2361582b0211e73d23f5dee6a2f68b7c2ba Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 11:31:05 -0700 Subject: [PATCH 077/139] docs(probes): queue M4.6, the start gate that cannot be released early Review found a real deadlock on a failure path. Every timer sizes a `Barrier` for all workers plus the coordinator, then spawns workers with `Scope::spawn`, which panics if the OS cannot create a thread. If that happens after an earlier worker has parked in `gate.wait()`, the coordinator never reaches its own `wait()`, the party count is never met, and `thread::scope` joins the parked worker while unwinding -- a join that never returns. The probe hangs rather than fails, which is the worse of the two. In the drained timers the consumer is parked on the same barrier and `StopOnDrop` cannot help, because the scope cannot finish unwinding to drop it. Queued rather than fixed, and the blocker is named in the item: `std::sync::Barrier` cannot be released short of its party count, so this is a change of primitive rather than a change of call -- and the current primitive's semantics are load-bearing for the measurement, since `measured_span`'s argument for why each worker times itself turns on `Barrier::wait` releasing every party together. Changing that mid-round would put the timing argument several review rounds have been read against back in play, to fix a path that needs the OS to refuse a thread. Raised rather than silently deferred, per the PRIME DIRECTIVE, and written into the checklist rather than left in a review thread, since a thread is not a work queue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index c7eb61805..e12c32384 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -172,6 +172,32 @@ correctness in the archive. Reported by review against this branch; the comment at the slotwise twin now states what the barrier actually guarantees rather than implying the window is closed. +- [ ] **M4.6** -- Make the start gate releasable, so a failed thread spawn cannot deadlock the probe. + + **Gap:** every timer sizes a `Barrier` for all planned workers plus the coordinator, then spawns + the workers with `Scope::spawn`, which **panics** if the OS cannot create a thread. If that + happens after an earlier worker has already parked in `gate.wait()`, the coordinator never reaches + its own `wait()`, so the party count is never met. `thread::scope` then joins the parked worker + while unwinding, and the join never returns: the probe hangs rather than fails. In the drained + timers the consumer is parked on the same barrier, and `StopOnDrop` cannot help, because the scope + cannot finish unwinding to drop it. + + **Target:** a gate that can be released short of its party count -- the coordinator must be able to + say "nobody else is coming" and have every parked participant return. `std::sync::Barrier` cannot + express that (a party count, once set, must be met), so this is a change of primitive rather than a + change of call, and it touches every timer plus `start_barrier`'s documented reasoning about what + the barrier guarantees. Prefer a specified primitive over a hand-rolled spin, per + [DESIGN-NOTES.md](../windows-waitable-queues/DESIGN-NOTES.md#d-40). + + **Why this is queued rather than taken:** it requires replacing a synchronisation primitive whose + current semantics are load-bearing for the measurement (see `measured_span`'s note on why each + worker times itself, which turns on `Barrier::wait` releasing every party together). Changing it + mid-review-round risks the timing argument that several rounds have already been read against, and + the failure path it fixes needs the OS to refuse a thread. Raised rather than silently deferred, + per the PRIME DIRECTIVE. + + Reported by review against this branch. + - [ ] **M2.5** -- Make the banner describe the read the body describes. Gated by M3.1 and M3.3, both landed: establishing that the middle of three discoveries agreed From a9b945bde723d56b4800d3335c45509d7ef1849d Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 16:30:03 -0400 Subject: [PATCH 078/139] fix(probes): replace the start barrier with a gate that can be released Completed item: M4.6: Make the start gate releasable, so a failed thread spawn cannot deadlock the probe. `std::sync::Barrier`'s party count, once set, must be met -- there is no way to say "nobody else is coming". The timers size the gate for every planned worker and then spawn them with `Scope::spawn`, which panics if the OS refuses a thread. If that happened after an earlier worker had parked, the coordinator never reached its own arrival, the count was never met, and `thread::scope` joined a permanently parked worker while unwinding. The probe hung rather than failed, which is the worse of the two. `StartGate` keeps the behaviour the measurement depends on -- a complete party releases every member together, which is what `measured_span` relies on when it argues that each worker must time itself -- and adds the operation `Barrier` lacks. `arrive_and_wait` now answers whether the party completed, so a worker freed by a release returns instead of timing an abandoned run. `ReleaseOnDrop`, held inside each scope's closure, performs the release while that closure unwinds, which is before the join loop it has to unblock. Poisoning is stepped over rather than propagated: nothing but the gate's own bookkeeping runs under that lock, so a poisoned mutex means another thread panicked while parked, and panicking on the way out of a `Drop` that is already unwinding would abort the process instead of unblocking it. Applied to all nine timers at once. They share the defect, and this branch has three times shipped a correct fix applied to a subset of its call sites. **Two of the four new tests were first written to assert on the test thread, and sabotage caught both**: with `release` neutered they parked the test rather than failing it, which would wedge a suite that runs its tests as threads in one process. Every gate test now arrives off-thread and polls for the answer, so a regression reddens in five seconds. Re-verified after the rewrite: neutering `release` fails exactly the two tests that depend on it, in 5.05s, while the complete-party test still passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 6 +- .../src/queue_contention.rs | 232 +++++++++++++++--- .../src/queue_contention/tests.rs | 111 +++++++++ 3 files changed, 310 insertions(+), 39 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index e12c32384..d44dc48f5 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -172,7 +172,11 @@ correctness in the archive. Reported by review against this branch; the comment at the slotwise twin now states what the barrier actually guarantees rather than implying the window is closed. -- [ ] **M4.6** -- Make the start gate releasable, so a failed thread spawn cannot deadlock the probe. +- [x] **M4.6** -- Make the start gate releasable, so a failed thread spawn cannot deadlock the probe. + `StartGate` replaces `std::sync::Barrier`: a complete party opens it as before, and the + coordinator can open it early when a spawn fails, so parked workers are freed instead of held + for arrivals that will never come. `ReleaseOnDrop` performs that release on the unwind path. + The property `measured_span` depends on -- every party released together -- is preserved. **Gap:** every timer sizes a `Barrier` for all planned workers plus the coordinator, then spawns the workers with `Scope::spawn`, which **panics** if the OS cannot create a thread. If that diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 17214e0f8..8bc6768f2 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -79,8 +79,8 @@ use std::fmt; use std::sync::Arc; -use std::sync::Barrier; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Condvar, Mutex, MutexGuard, PoisonError}; use std::thread; use std::time::Instant; @@ -745,14 +745,18 @@ fn time_contended_atomic(producers: usize) -> Repetition { // here; the clock starts as the barrier releases, so neither thread creation // nor a solo head start by an early worker is inside the measurement. See // `start_barrier`'s note for why that matters at these producer counts. - let gate = Arc::new(Barrier::new(producers + 1)); + let gate = start_gate(producers); let spans = thread::scope(|scope| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); let mut workers = Vec::with_capacity(producers); for _ in 0..producers { let counter = Arc::clone(&counter); let gate = Arc::clone(&gate); workers.push(scope.spawn(move || { - gate.wait(); + if !gate.arrive_and_wait() { + // Abandoned before the party completed; see `StartGate`. + return (Instant::now(), Instant::now()); + } let began = Instant::now(); for _ in 0..PUSHES_PER_PRODUCER { counter.fetch_add(1, Ordering::Relaxed); @@ -760,7 +764,7 @@ fn time_contended_atomic(producers: usize) -> Repetition { (began, Instant::now()) })); } - gate.wait(); + let _ = gate.arrive_and_wait(); workers .into_iter() .map(|worker| worker.join().expect("a producer must not panic")) @@ -774,7 +778,8 @@ fn capacity_for(producers: usize) -> usize { (producers * PUSHES_PER_PRODUCER).next_power_of_two() } -/// A gate holding every participant until all of them exist. +/// A gate holding every participant until all of them exist, or until the +/// coordinator gives up on the ones that do not. /// /// **Without this the row labelled N producers need not have measured N of /// them.** Spawning is not instant, and each worker used to start pushing the @@ -785,11 +790,122 @@ fn capacity_for(producers: usize) -> usize { /// output of this probe, and both effects bend it downward exactly where it is /// steepest. /// -/// The count includes this thread, so no worker can start before the last one -/// exists. It does NOT start the clock -- see [`measured_span`] for why that is -/// a separate job. -fn start_barrier(participants: usize) -> Arc { - Arc::new(Barrier::new(participants + 1)) +/// The count includes the coordinating thread, so no worker can start before the +/// last one exists. It does NOT start the clock -- see [`measured_span`] for why +/// that is a separate job. +/// +/// **Why this is not `std::sync::Barrier`.** A `Barrier`'s party count, once +/// set, must be met: there is no way to say "nobody else is coming". The timers +/// size the gate for every planned worker and then spawn them with +/// `Scope::spawn`, which *panics* if the OS refuses a thread. If that happened +/// after an earlier worker had already parked, the coordinator never reached its +/// own arrival, the count was never met, and `thread::scope` joined a +/// permanently parked worker while unwinding -- so the probe **hung rather than +/// failed**, which is the worse of the two. [`StartGate::release`] is the +/// missing operation, and [`ReleaseOnDrop`] performs it on the unwind path. +/// +/// **The property the measurement depends on is preserved.** Every parked party +/// is woken by one `notify_all` and returns as a group, exactly as +/// `Barrier::wait` does -- which is what [`measured_span`] relies on when it +/// argues that this thread cannot time the workers and each must time itself. +struct StartGate { + state: Mutex, + opened: Condvar, +} + +struct GateState { + /// Participants still to arrive. Reaching zero opens the gate. + remaining: usize, + /// Whether waiters may proceed, for either reason. + open: bool, + /// Whether the gate opened because everyone arrived, rather than because + /// the coordinator released it. A participant that reads `false` learns its + /// run was abandoned and should not do the work. + complete: bool, +} + +impl StartGate { + /// `participants` workers plus the coordinating thread. + fn new(participants: usize) -> Arc { + Arc::new(Self { + state: Mutex::new(GateState { + remaining: participants + 1, + open: false, + complete: false, + }), + opened: Condvar::new(), + }) + } + + /// Poisoning is stepped over rather than propagated. + /// + /// Nothing but the gate's own bookkeeping runs under this lock, so a + /// poisoned mutex means some *other* thread panicked while parked here. The + /// whole point of this type is to unblock that situation; panicking on the + /// way -- from inside a `Drop` that is already unwinding -- would abort the + /// process instead. + fn locked(&self) -> MutexGuard<'_, GateState> { + self.state.lock().unwrap_or_else(PoisonError::into_inner) + } + + /// Arrive, then block until every participant has, or until the gate is + /// released. + /// + /// `true` when the gate opened because the party was complete, which is the + /// only case in which a run's timings mean anything. `false` says the + /// coordinator gave up; the caller should return without doing the work. + #[must_use] + fn arrive_and_wait(&self) -> bool { + let mut state = self.locked(); + state.remaining = state.remaining.saturating_sub(1); + if state.remaining == 0 && !state.open { + state.open = true; + state.complete = true; + } + if state.open { + let complete = state.complete; + drop(state); + self.opened.notify_all(); + return complete; + } + while !state.open { + state = self + .opened + .wait(state) + .unwrap_or_else(PoisonError::into_inner); + } + state.complete + } + + /// Open the gate now, however many participants are missing. + /// + /// A no-op once the gate is open, so the guard that calls this on the + /// ordinary path costs nothing. + fn release(&self) { + let mut state = self.locked(); + state.open = true; + drop(state); + self.opened.notify_all(); + } +} + +/// Releases the start gate however the spawning phase ends. +/// +/// The failure this exists for is a `Scope::spawn` panic partway through +/// creating the workers: without it, the parties already parked wait for a count +/// that will never be met, and the join that `thread::scope` performs while +/// unwinding never returns. Held inside the scope's closure, so it drops while +/// that closure unwinds -- before the join loop it needs to unblock. +struct ReleaseOnDrop(Arc); + +impl Drop for ReleaseOnDrop { + fn drop(&mut self) { + self.0.release(); + } +} + +fn start_gate(participants: usize) -> Arc { + StartGate::new(participants) } /// The wall-clock window the producers were actually inside: from the first to @@ -831,14 +947,18 @@ fn measured_span(spans: &[(Instant, Instant)]) -> f64 { fn time_isolated_mpsc(producers: usize) -> Repetition { let (tx, rx) = slotwise_mpsc::bounded::(capacity_for(producers)).expect("a valid capacity"); - let gate = start_barrier(producers); + let gate = start_gate(producers); let spans = thread::scope(|scope| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); workers.push(scope.spawn(move || { - gate.wait(); + if !gate.arrive_and_wait() { + // Abandoned before the party completed; see `StartGate`. + return (Instant::now(), Instant::now()); + } let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) @@ -847,7 +967,7 @@ fn time_isolated_mpsc(producers: usize) -> Repetition { (began, Instant::now()) })); } - gate.wait(); + let _ = gate.arrive_and_wait(); workers .into_iter() .map(|worker| worker.join().expect("a producer must not panic")) @@ -864,14 +984,18 @@ fn time_isolated_mpsc(producers: usize) -> Repetition { fn time_isolated_reserving(producers: usize) -> Repetition { let (tx, rx) = reserving_mpsc::bounded::(capacity_for(producers)).expect("a valid capacity"); - let gate = start_barrier(producers); + let gate = start_gate(producers); let spans = thread::scope(|scope| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); workers.push(scope.spawn(move || { - gate.wait(); + if !gate.arrive_and_wait() { + // Abandoned before the party completed; see `StartGate`. + return (Instant::now(), Instant::now()); + } let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) @@ -880,7 +1004,7 @@ fn time_isolated_reserving(producers: usize) -> Repetition { (began, Instant::now()) })); } - gate.wait(); + let _ = gate.arrive_and_wait(); workers .into_iter() .map(|worker| worker.join().expect("a producer must not panic")) @@ -905,14 +1029,18 @@ fn time_isolated_reserving(producers: usize) -> Repetition { /// a difference of a few nanoseconds per push. fn time_isolated_permit(producers: usize) -> Repetition { let (tx, rx) = permit_mpsc::bounded::(capacity_for(producers)).expect("a valid capacity"); - let gate = start_barrier(producers); + let gate = start_gate(producers); let spans = thread::scope(|scope| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); workers.push(scope.spawn(move || { - gate.wait(); + if !gate.arrive_and_wait() { + // Abandoned before the party completed; see `StartGate`. + return (Instant::now(), Instant::now()); + } let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) @@ -921,7 +1049,7 @@ fn time_isolated_permit(producers: usize) -> Repetition { (began, Instant::now()) })); } - gate.wait(); + let _ = gate.arrive_and_wait(); workers .into_iter() .map(|worker| worker.join().expect("a producer must not panic")) @@ -979,11 +1107,13 @@ fn time_drained_mpsc(producers: usize) -> Repetition { // needs a readiness flag the producers spin on, which would change the // measurement and so obsolete every figure already published against it -- // queued as M4.3 rather than taken mid-branch. - let gate = start_barrier(producers + 1); + let gate = start_gate(producers + 1); let consumer_gate = Arc::clone(&gate); let consumer = thread::spawn(move || { - consumer_gate.wait(); + if !consumer_gate.arrive_and_wait() { + return rx.refused(); + } // Spin rather than park: the doorbell's cost is `doorbell_cost`'s // question, and parking here would measure that instead of the claim. while !consumer_done.load(Ordering::Relaxed) { @@ -995,12 +1125,16 @@ fn time_drained_mpsc(producers: usize) -> Repetition { }); let spans = thread::scope(|scope| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); workers.push(scope.spawn(move || { - gate.wait(); + if !gate.arrive_and_wait() { + // Abandoned before the party completed; see `StartGate`. + return (Instant::now(), Instant::now()); + } let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; @@ -1025,7 +1159,7 @@ fn time_drained_mpsc(producers: usize) -> Repetition { (began, Instant::now()) })); } - gate.wait(); + let _ = gate.arrive_and_wait(); workers .into_iter() .map(|worker| worker.join().expect("a producer must not panic")) @@ -1067,11 +1201,13 @@ fn time_drained_reserving(producers: usize) -> Repetition { let stop = StopOnDrop(done); // The consumer joins the gate here for the reason it does in the slotwise // twin: a run whose opening is undrained is not the regime being measured. - let gate = start_barrier(producers + 1); + let gate = start_gate(producers + 1); let consumer_gate = Arc::clone(&gate); let consumer = thread::spawn(move || { - consumer_gate.wait(); + if !consumer_gate.arrive_and_wait() { + return rx.refused(); + } while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} std::hint::spin_loop(); @@ -1081,12 +1217,16 @@ fn time_drained_reserving(producers: usize) -> Repetition { }); let spans = thread::scope(|scope| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); workers.push(scope.spawn(move || { - gate.wait(); + if !gate.arrive_and_wait() { + // Abandoned before the party completed; see `StartGate`. + return (Instant::now(), Instant::now()); + } let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; @@ -1105,7 +1245,7 @@ fn time_drained_reserving(producers: usize) -> Repetition { (began, Instant::now()) })); } - gate.wait(); + let _ = gate.arrive_and_wait(); workers .into_iter() .map(|worker| worker.join().expect("a producer must not panic")) @@ -1134,11 +1274,13 @@ fn time_drained_permit(producers: usize) -> Repetition { let consumer_done = Arc::clone(&done); // Set on every exit path, not just the one that returns. See StopOnDrop. let stop = StopOnDrop(done); - let gate = start_barrier(producers + 1); + let gate = start_gate(producers + 1); let consumer_gate = Arc::clone(&gate); let consumer = thread::spawn(move || { - consumer_gate.wait(); + if !consumer_gate.arrive_and_wait() { + return rx.refused(); + } while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} std::hint::spin_loop(); @@ -1148,12 +1290,16 @@ fn time_drained_permit(producers: usize) -> Repetition { }); let spans = thread::scope(|scope| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); workers.push(scope.spawn(move || { - gate.wait(); + if !gate.arrive_and_wait() { + // Abandoned before the party completed; see `StartGate`. + return (Instant::now(), Instant::now()); + } let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; @@ -1172,7 +1318,7 @@ fn time_drained_permit(producers: usize) -> Repetition { (began, Instant::now()) })); } - gate.wait(); + let _ = gate.arrive_and_wait(); workers .into_iter() .map(|worker| worker.join().expect("a producer must not panic")) @@ -1209,14 +1355,18 @@ fn time_drained_permit(producers: usize) -> Repetition { fn time_isolated_layout(producers: usize) -> Repetition { let (tx, rx) = reserving_mpsc::bounded_as::(capacity_for(producers)).expect("a valid capacity"); - let gate = start_barrier(producers); + let gate = start_gate(producers); let spans = thread::scope(|scope| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); workers.push(scope.spawn(move || { - gate.wait(); + if !gate.arrive_and_wait() { + // Abandoned before the party completed; see `StartGate`. + return (Instant::now(), Instant::now()); + } let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { tx.push((producer * PUSHES_PER_PRODUCER + index) as u64) @@ -1225,7 +1375,7 @@ fn time_isolated_layout(producers: usize) -> Repetition { (began, Instant::now()) })); } - gate.wait(); + let _ = gate.arrive_and_wait(); workers .into_iter() .map(|worker| worker.join().expect("a producer must not panic")) @@ -1249,11 +1399,13 @@ fn time_drained_layout(producers: usize) -> Repetition let stop = StopOnDrop(done); // The consumer joins the gate for the reason its twins do: a run whose // opening is undrained is not the regime being measured. - let gate = start_barrier(producers + 1); + let gate = start_gate(producers + 1); let consumer_gate = Arc::clone(&gate); let consumer = thread::spawn(move || { - consumer_gate.wait(); + if !consumer_gate.arrive_and_wait() { + return rx.refused(); + } while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} std::hint::spin_loop(); @@ -1263,12 +1415,16 @@ fn time_drained_layout(producers: usize) -> Repetition }); let spans = thread::scope(|scope| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); let mut workers = Vec::with_capacity(producers); for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); workers.push(scope.spawn(move || { - gate.wait(); + if !gate.arrive_and_wait() { + // Abandoned before the party completed; see `StartGate`. + return (Instant::now(), Instant::now()); + } let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; @@ -1287,7 +1443,7 @@ fn time_drained_layout(producers: usize) -> Repetition (began, Instant::now()) })); } - gate.wait(); + let _ = gate.arrive_and_wait(); workers .into_iter() .map(|worker| worker.join().expect("a producer must not panic")) diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 5365a7df4..c3b4b5f94 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -1340,3 +1340,114 @@ fn stop_on_drop_sets_the_flag_when_the_producer_phase_unwinds() { "the consumer would spin forever on a flag nobody sets" ); } + +/// Arrives at a gate on its own thread and hands back somewhere to read the +/// answer. +/// +/// **Every gate test goes through this rather than calling `arrive_and_wait` +/// directly.** A gate regression parks its caller forever, so an assertion made +/// on the test thread would hang the whole suite -- which runs its tests as +/// threads in one process -- instead of failing it. The spawned thread is +/// deliberately never joined, for the same reason. +/// +/// Two of these tests were first written the direct way, and sabotage caught +/// both: neutering `release` wedged the run rather than reddening it. +fn spawn_arrival(gate: &Arc) -> Arc>> { + let gate = Arc::clone(gate); + let outcome = Arc::new(Mutex::new(None)); + let observed = Arc::clone(&outcome); + thread::spawn(move || { + let complete = gate.arrive_and_wait(); + *observed.lock().expect("not poisoned") = Some(complete); + }); + outcome +} + +/// The answer from [`spawn_arrival`], or `None` if the party never came back. +fn await_arrival(outcome: &Arc>>) -> Option { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + if let Some(complete) = *outcome.lock().expect("not poisoned") { + return Some(complete); + } + if Instant::now() >= deadline { + return None; + } + thread::sleep(Duration::from_millis(10)); + } +} + +/// A complete party opens the gate and every member learns that it was complete. +#[test] +fn a_complete_party_opens_the_gate_for_everyone() { + let gate = StartGate::new(3); + let arrivals: Vec<_> = (0..4).map(|_| spawn_arrival(&gate)).collect(); + for outcome in &arrivals { + assert_eq!( + await_arrival(outcome), + Some(true), + "every member of a complete party must be freed, and told so" + ); + } +} + +/// A released gate frees parties waiting for arrivals that will never come. +/// +/// This is the deadlock `M4.6` was queued for, reproduced without needing the +/// OS to refuse a thread: the gate is sized for a party that never completes, +/// which is what a panicking `Scope::spawn` leaves behind. +#[test] +fn a_released_gate_frees_parties_that_will_never_be_completed() { + let gate = StartGate::new(2); + let parked = spawn_arrival(&gate); + + // Let it reach the gate, then give up on the members that never arrive. + thread::sleep(Duration::from_millis(50)); + gate.release(); + + assert_eq!( + await_arrival(&parked), + Some(false), + "the parked party must be freed and told the party was incomplete, so \ + a worker knows its run was abandoned" + ); +} + +/// Arriving at an already-released gate reports the party incomplete. +/// +/// The ordering matters: a worker spawned before the failure may arrive after +/// the coordinator has given up, and must reach the same conclusion as one that +/// was already parked. +#[test] +fn arriving_after_a_release_still_reports_an_incomplete_party() { + let gate = StartGate::new(4); + gate.release(); + for _ in 0..2 { + assert_eq!( + await_arrival(&spawn_arrival(&gate)), + Some(false), + "a late arrival must not be told the party completed, and the \ + answer must not change on a second look" + ); + } +} + +/// The guard releases the gate while its scope unwinds. +#[test] +fn release_on_drop_frees_the_gate_when_spawning_panics() { + let gate = StartGate::new(8); + let parked = spawn_arrival(&gate); + thread::sleep(Duration::from_millis(50)); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _release = ReleaseOnDrop(Arc::clone(&gate)); + panic!("the OS refused a thread, as `Scope::spawn` does by panicking"); + })); + assert!(result.is_err(), "the fixture must actually unwind"); + + assert_eq!( + await_arrival(&parked), + Some(false), + "the guard did not release the gate as its scope unwound" + ); +} From 04e6d825919641fa597090333fe3eda2f164b32c Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 16:41:00 -0400 Subject: [PATCH 079/139] fix(probes): hold producers until the consumer says it is draining The first half of M4.3. The gate proves the consumer exists, is scheduled and is past thread start-up; it does not prove the consumer has reached its first `pop`, and it releases every party together. So a producer could push into a queue nobody was draining yet -- an undrained opening to a run whose whole subject is that it is drained. The window was bounded by a scheduling quantum rather than by thread creation, which is why the gate was still worth having, but it was not zero. `await_consumer` closes it: the consumer announces that it is draining, and producers hold until they see that before starting their clocks. Applied to all four drained timers, and the comment in the slotwise twin that the other three point at now describes what is actually guaranteed. `Acquire`/`Release` rather than `Relaxed`, though the flag carries no data, per the queue crate's D-40 standing answer: an acquire that proves unnecessary costs little, while a relaxed load that turns out to have been load-bearing fails only on hardware nobody here owns. **This moves the drained numbers**, which is the rest of the item: the capture and the design-note amendments follow in the next commit, because a capture cannot cite the commit of the instrument that produced it until that commit exists. The figures taken before this change are kept beside the ones taken after rather than replaced -- they measure two different pieces of code, and both are real. Verified by sabotage: making `await_consumer` return without waiting fails the new test on the "has not announced itself yet" assertion, which is the half that a test written in the convenient order would have missed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/queue_contention.rs | 79 ++++++++++++++++--- .../src/queue_contention/tests.rs | 43 ++++++++++ 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 8bc6768f2..f556c1ae0 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -1088,25 +1088,55 @@ impl Drop for StopOnDrop { } } +/// Holds a producer until the consumer is actually draining. +/// +/// **The gate is not enough, and the difference is the regime being measured.** +/// Arriving at the gate proves the consumer exists, is scheduled and is past +/// thread start-up; it does not prove the consumer has reached its first `pop`. +/// The gate releases every party together, so a producer could push into a queue +/// nobody was taking from yet -- an undrained opening to a run whose whole point +/// is that it is drained. The window was bounded by a scheduling quantum rather +/// than by thread creation, which is why the gate was still worth having, but it +/// was not zero. +/// +/// `Acquire`/`Release` rather than `Relaxed`, though the flag carries no data: +/// this is the standing "promote the load" answer recorded in the queue crate's +/// [D-40](../../windows-waitable-queues/DESIGN-NOTES.md#d-40) -- an acquire that +/// proves unnecessary costs little, while a relaxed load that turns out to have +/// been load-bearing fails only on hardware nobody here owns. +/// +/// **This changes what the drained rows measure**, which is why it is `M4.3` and +/// why the figures taken before it are kept beside the ones taken after rather +/// than replaced: they are measurements of two different pieces of code, and +/// both are real. +fn await_consumer(ready: &AtomicBool) { + while !ready.load(Ordering::Acquire) { + std::hint::spin_loop(); + } +} + fn time_drained_mpsc(producers: usize) -> Repetition { let (tx, rx) = slotwise_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); let done = Arc::new(AtomicBool::new(false)); let consumer_done = Arc::clone(&done); // Set on every exit path, not just the one that returns. See StopOnDrop. let stop = StopOnDrop(done); - // The consumer is a barrier participant, not merely spawned: spawning is not + // Closes the undrained opening the gate alone leaves. See await_consumer. + let ready = Arc::new(AtomicBool::new(false)); + let consumer_ready = Arc::clone(&ready); + // The consumer is a gate participant, not merely spawned: spawning is not // readiness, and a consumer still in thread start-up while producers push // turns the opening of the run into an undrained regime. // - // Be precise about what this buys, because it is less than it looks. The - // barrier guarantees the consumer has ARRIVED -- it exists, is scheduled, and - // is past start-up -- not that it reaches its first `pop` before a producer - // reaches its first `push`. A release wakes every party at once, so a short - // undrained window remains. It is bounded by a scheduling quantum rather than - // by thread creation, which is the improvement; it is not zero. Closing it - // needs a readiness flag the producers spin on, which would change the - // measurement and so obsolete every figure already published against it -- - // queued as M4.3 rather than taken mid-branch. + // The gate alone does not finish the job, which is why `await_consumer` + // exists. Arriving proves the consumer exists, is scheduled and is past + // start-up; it does not prove the consumer has reached its first `pop`, and + // the gate releases every party together. The handshake closes that + // remainder: the consumer announces that it is draining, and producers hold + // until they see it. This is the M4.3 change, and it MOVED the drained + // numbers -- the figures taken before it are kept beside the ones taken + // after rather than replaced, because they measure two different pieces of + // code. let gate = start_gate(producers + 1); let consumer_gate = Arc::clone(&gate); @@ -1114,6 +1144,9 @@ fn time_drained_mpsc(producers: usize) -> Repetition { if !consumer_gate.arrive_and_wait() { return rx.refused(); } + // Announced before the drain loop, so producers start against a + // consumer that is running rather than one merely spawned. + consumer_ready.store(true, Ordering::Release); // Spin rather than park: the doorbell's cost is `doorbell_cost`'s // question, and parking here would measure that instead of the claim. while !consumer_done.load(Ordering::Relaxed) { @@ -1130,11 +1163,13 @@ fn time_drained_mpsc(producers: usize) -> Repetition { for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); + let ready = Arc::clone(&ready); workers.push(scope.spawn(move || { if !gate.arrive_and_wait() { // Abandoned before the party completed; see `StartGate`. return (Instant::now(), Instant::now()); } + await_consumer(&ready); let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; @@ -1199,6 +1234,9 @@ fn time_drained_reserving(producers: usize) -> Repetition { let consumer_done = Arc::clone(&done); // Set on every exit path, not just the one that returns. See StopOnDrop. let stop = StopOnDrop(done); + // Closes the undrained opening the gate alone leaves. See await_consumer. + let ready = Arc::new(AtomicBool::new(false)); + let consumer_ready = Arc::clone(&ready); // The consumer joins the gate here for the reason it does in the slotwise // twin: a run whose opening is undrained is not the regime being measured. let gate = start_gate(producers + 1); @@ -1208,6 +1246,9 @@ fn time_drained_reserving(producers: usize) -> Repetition { if !consumer_gate.arrive_and_wait() { return rx.refused(); } + // Announced before the drain loop, so producers start against a + // consumer that is running rather than one merely spawned. + consumer_ready.store(true, Ordering::Release); while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} std::hint::spin_loop(); @@ -1222,11 +1263,13 @@ fn time_drained_reserving(producers: usize) -> Repetition { for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); + let ready = Arc::clone(&ready); workers.push(scope.spawn(move || { if !gate.arrive_and_wait() { // Abandoned before the party completed; see `StartGate`. return (Instant::now(), Instant::now()); } + await_consumer(&ready); let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; @@ -1274,6 +1317,9 @@ fn time_drained_permit(producers: usize) -> Repetition { let consumer_done = Arc::clone(&done); // Set on every exit path, not just the one that returns. See StopOnDrop. let stop = StopOnDrop(done); + // Closes the undrained opening the gate alone leaves. See await_consumer. + let ready = Arc::new(AtomicBool::new(false)); + let consumer_ready = Arc::clone(&ready); let gate = start_gate(producers + 1); let consumer_gate = Arc::clone(&gate); @@ -1281,6 +1327,9 @@ fn time_drained_permit(producers: usize) -> Repetition { if !consumer_gate.arrive_and_wait() { return rx.refused(); } + // Announced before the drain loop, so producers start against a + // consumer that is running rather than one merely spawned. + consumer_ready.store(true, Ordering::Release); while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} std::hint::spin_loop(); @@ -1295,11 +1344,13 @@ fn time_drained_permit(producers: usize) -> Repetition { for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); + let ready = Arc::clone(&ready); workers.push(scope.spawn(move || { if !gate.arrive_and_wait() { // Abandoned before the party completed; see `StartGate`. return (Instant::now(), Instant::now()); } + await_consumer(&ready); let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; @@ -1397,6 +1448,9 @@ fn time_drained_layout(producers: usize) -> Repetition let consumer_done = Arc::clone(&done); // Set on every exit path, not just the one that returns. See StopOnDrop. let stop = StopOnDrop(done); + // Closes the undrained opening the gate alone leaves. See await_consumer. + let ready = Arc::new(AtomicBool::new(false)); + let consumer_ready = Arc::clone(&ready); // The consumer joins the gate for the reason its twins do: a run whose // opening is undrained is not the regime being measured. let gate = start_gate(producers + 1); @@ -1406,6 +1460,9 @@ fn time_drained_layout(producers: usize) -> Repetition if !consumer_gate.arrive_and_wait() { return rx.refused(); } + // Announced before the drain loop, so producers start against a + // consumer that is running rather than one merely spawned. + consumer_ready.store(true, Ordering::Release); while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} std::hint::spin_loop(); @@ -1420,11 +1477,13 @@ fn time_drained_layout(producers: usize) -> Repetition for producer in 0..producers { let tx = tx.clone(); let gate = Arc::clone(&gate); + let ready = Arc::clone(&ready); workers.push(scope.spawn(move || { if !gate.arrive_and_wait() { // Abandoned before the party completed; see `StartGate`. return (Instant::now(), Instant::now()); } + await_consumer(&ready); let began = Instant::now(); for index in 0..PUSHES_PER_PRODUCER { let mut item = (producer * PUSHES_PER_PRODUCER + index) as u64; diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index c3b4b5f94..1ca42b839 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -1451,3 +1451,46 @@ fn release_on_drop_frees_the_gate_when_spawning_panics() { "the guard did not release the gate as its scope unwound" ); } + +/// A producer does not start timing until the consumer says it is draining. +/// +/// This is the window `M4.3` closed: the gate proves the consumer exists and is +/// scheduled, not that it has reached its first `pop`, so a producer released by +/// the gate could push into a queue nobody was draining yet -- an undrained +/// opening to a run whose whole subject is that it is drained. +/// +/// Asserted by holding the flag clear and showing the producer stays put, then +/// setting it and showing the producer moves. A test that only set the flag +/// first would pass against a missing handshake, which is the shape that has +/// slipped through on this branch before. +#[test] +fn a_producer_waits_for_the_consumer_to_announce_that_it_is_draining() { + let ready = Arc::new(AtomicBool::new(false)); + let waited = Arc::new(AtomicBool::new(false)); + + let consumer_ready = Arc::clone(&ready); + let observed = Arc::clone(&waited); + // Detached rather than joined: a regression leaves this parked forever, and + // joining would hang the suite instead of failing it. + thread::spawn(move || { + await_consumer(&consumer_ready); + observed.store(true, Ordering::Release); + }); + + // While the consumer has not announced itself, the producer must not pass. + thread::sleep(Duration::from_millis(100)); + assert!( + !waited.load(Ordering::Acquire), + "a producer started before the consumer was draining" + ); + + ready.store(true, Ordering::Release); + let deadline = Instant::now() + Duration::from_secs(5); + while !waited.load(Ordering::Acquire) { + assert!( + Instant::now() < deadline, + "the producer never observed the consumer's announcement" + ); + thread::sleep(Duration::from_millis(10)); + } +} From 9e84012646aea3b4b6db0d9d453defe81a73c161 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 16:49:21 -0400 Subject: [PATCH 080/139] docs(probes): amend the drained figures with a post-handshake capture Completed item: M4.3: Close the undrained window at the start of the drained regime with a readiness handshake, and re-measure everything that changes. Completed item: M4.6: Make the start gate releasable, so a failed thread spawn cannot deadlock the probe. The second half of M4.3. `04e6d825` changed what the drained rows measure, so the figures published against the old instrument are now measurements of code that no longer exists. **Amended rather than replaced, because this is new data and not a correction.** The earlier capture remains what the earlier instrument measured; the new one is beside it, and each is labelled with the code that produced it. Withdrawing the first would discard a real observation to make the record look tidier. Re-measured on the same host as the crate README's capture, three whole-probe invocations, committed at `captures/2026-09-16-drained-handshake/`. The raw reports go in whole rather than trimmed to the interesting rows, with the summarising script beside them and its output regenerable by one command -- so the derivation can be checked instead of trusted, and nothing downstream has to retype a figure. The two captures are not a like-for-like comparison of dispersion, seven runs against three, so the amendment records that the measurement changed rather than that the spread moved. The finding survived: every layout median still sits inside the same-code control band in the drained regime. Worth stating precisely, because it was not guaranteed -- the drained conclusion did not depend on the window it had been measured through. The queue crate's drained statements are deliberately untouched. They point at the probe's note rather than restating its figures, so they inherit the amendment; adding it there too would give one fact a second hand-maintained home, which is the defect rule 4 exists to stop. M4.3 and M4.6 are both large completed items, so both are archived with one-line stubs left in dependency order, per the move-with-link rule. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 57 +---- .../COMPLETED-CHECKLIST.md | 51 +++++ .../windows-platform-probes/DESIGN-NOTES.md | 23 ++ .../2026-09-16-drained-handshake/README.md | 43 ++++ .../2026-09-16-drained-handshake/run1.txt | 209 ++++++++++++++++++ .../2026-09-16-drained-handshake/run2.txt | 209 ++++++++++++++++++ .../2026-09-16-drained-handshake/run3.txt | 209 ++++++++++++++++++ .../2026-09-16-drained-handshake/summarise.js | 94 ++++++++ .../2026-09-16-drained-handshake/summary.txt | 18 ++ 9 files changed, 858 insertions(+), 55 deletions(-) create mode 100644 crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md create mode 100644 crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run1.txt create mode 100644 crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run2.txt create mode 100644 crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run3.txt create mode 100644 crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js create mode 100644 crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index d44dc48f5..88292b9aa 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -145,62 +145,9 @@ correctness in the archive. - [x] **M4.5** -- Emit the dispersion, not just the median. -> [completed 2026-09-15 UTC-07:00](COMPLETED-CHECKLIST.md#m45) -- [ ] **M4.3** -- Close the undrained window at the start of the drained regime with a readiness - handshake, and re-measure everything that changes. - - **Gap:** the drained timings put the consumer in the same `Barrier` as the producers, which - guarantees it has *arrived* -- spawned, scheduled, past thread start-up -- but not that it reaches - its first `pop` before a producer reaches its first `push`. The barrier releases every party at - once, so a short undrained window remains at the opening of each run. It is bounded by a - scheduling quantum rather than by thread creation, which is why the barrier is still worth having, - but it is not zero, and the drained regime is defined against exactly this. - - **Target:** the consumer sets an `AtomicBool` after entering its drain loop; producers spin on it - after `gate.wait()` and before `Instant::now()`. Apply it to all four drained functions - (`time_drained_mpsc`, `time_drained_reserving`, `time_drained_permit`, `time_drained_layout`) -- - they share the defect and the three siblings currently point at the slotwise twin's comment for - the reasoning, so that comment is the one to update. - - **BLOCKER, and the reason this is queued rather than taken:** adding the handshake changes the - measurement, so every drained figure already published in - [DESIGN-NOTES.md](DESIGN-NOTES.md) -- and the withdrawal argument built on the drained control -- - becomes a measurement of different code. The item is therefore "change it *and* re-run the - seven-run sweep *and* rewrite the drained sections", not a one-line fix, and doing it mid-branch - would invalidate figures that five review rounds have already been read against. Raised rather - than silently deferred, per the PRIME DIRECTIVE. +- [x] **M4.3** -- Close the undrained window at the start of the drained regime with a readiness handshake, and re-measure everything that changes. -> [completed 2026-09-16 UTC-04:00](COMPLETED-CHECKLIST.md#m43) - Reported by review against this branch; the comment at the slotwise twin now states what the - barrier actually guarantees rather than implying the window is closed. - -- [x] **M4.6** -- Make the start gate releasable, so a failed thread spawn cannot deadlock the probe. - `StartGate` replaces `std::sync::Barrier`: a complete party opens it as before, and the - coordinator can open it early when a spawn fails, so parked workers are freed instead of held - for arrivals that will never come. `ReleaseOnDrop` performs that release on the unwind path. - The property `measured_span` depends on -- every party released together -- is preserved. - - **Gap:** every timer sizes a `Barrier` for all planned workers plus the coordinator, then spawns - the workers with `Scope::spawn`, which **panics** if the OS cannot create a thread. If that - happens after an earlier worker has already parked in `gate.wait()`, the coordinator never reaches - its own `wait()`, so the party count is never met. `thread::scope` then joins the parked worker - while unwinding, and the join never returns: the probe hangs rather than fails. In the drained - timers the consumer is parked on the same barrier, and `StopOnDrop` cannot help, because the scope - cannot finish unwinding to drop it. - - **Target:** a gate that can be released short of its party count -- the coordinator must be able to - say "nobody else is coming" and have every parked participant return. `std::sync::Barrier` cannot - express that (a party count, once set, must be met), so this is a change of primitive rather than a - change of call, and it touches every timer plus `start_barrier`'s documented reasoning about what - the barrier guarantees. Prefer a specified primitive over a hand-rolled spin, per - [DESIGN-NOTES.md](../windows-waitable-queues/DESIGN-NOTES.md#d-40). - - **Why this is queued rather than taken:** it requires replacing a synchronisation primitive whose - current semantics are load-bearing for the measurement (see `measured_span`'s note on why each - worker times itself, which turns on `Barrier::wait` releasing every party together). Changing it - mid-review-round risks the timing argument that several rounds have already been read against, and - the failure path it fixes needs the OS to refuse a thread. Raised rather than silently deferred, - per the PRIME DIRECTIVE. - - Reported by review against this branch. +- [x] **M4.6** -- Make the start gate releasable, so a failed thread spawn cannot deadlock the probe. -> [completed 2026-09-16 UTC-04:00](COMPLETED-CHECKLIST.md#m46) - [ ] **M2.5** -- Make the banner describe the read the body describes. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index f4fd43bdc..862de0de7 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1151,3 +1151,54 @@ instead of the minimum fails `median_run_carries_the_fastest_and_slowest_repetit The dispersion justified itself on first capture. `slotwise_mpsc` at two producers spans 19.3 to 59.5 ns/op within one configuration on one host, which the median alone had concealed entirely, in a table that had already been published twice. + +## Moved 2026-09-16 16:46:50 UTC-04:00 -- M4.6 and M4.3: the start gate, and the window it left open + +### M4.6 -- Make the start gate releasable, so a failed thread spawn cannot deadlock the probe. *(completed 2026-09-16 16:46:50 UTC-04:00)* + +Every timer sized a `std::sync::Barrier` for all planned workers plus the coordinator, then spawned +the workers with `Scope::spawn`, which **panics** if the OS cannot create a thread. If that happened +after an earlier worker had already parked, the coordinator never reached its own arrival, the party +count was never met, and `thread::scope` joined a permanently parked worker while unwinding. The +probe hung rather than failed, which is the worse of the two. In the drained timers the consumer was +parked on the same barrier, and `StopOnDrop` could not help, because the scope could not finish +unwinding to drop it. + +A `Barrier`'s party count, once set, must be met, so the fix was a change of primitive rather than a +change of call. `StartGate` keeps the property the measurement depends on -- a complete party +releases every member together, which is what `measured_span` relies on when it argues that each +worker must time itself -- and adds the operation `Barrier` lacks. `arrive_and_wait` now reports +whether the party completed, so a worker freed by a release returns instead of timing an abandoned +run; `ReleaseOnDrop`, held inside each scope's closure, performs the release while that closure +unwinds, which is before the join loop it has to unblock. Poisoning is stepped over rather than +propagated, because panicking out of a `Drop` that is already unwinding would abort the process +instead of unblocking it. + +Applied to all nine timers at once, since they share the defect and this branch had three times +shipped a correct fix applied to a subset of its call sites. + +**Two of the four new tests were first written to assert on the test thread, and sabotage caught +both**: with `release` neutered they parked the test rather than failing it, which would wedge a +suite that runs its tests as threads in one process. Every gate test now arrives off-thread and +polls, so a regression reddens in five seconds. + +### M4.3 -- Close the undrained window at the start of the drained regime with a readiness handshake, and re-measure everything that changes. *(completed 2026-09-16 16:46:50 UTC-04:00)* + +The gate proves the consumer exists, is scheduled and is past thread start-up; it does not prove the +consumer has reached its first `pop`, and it releases every party together. So a producer could push +into a queue nobody was draining yet -- an undrained opening to a run whose whole subject is that it +is drained. `await_consumer` closes it: the consumer announces that it is draining, and producers +hold until they see that before starting their clocks. Applied to all four drained timers, with +`Acquire`/`Release` per the queue crate's `D-40` standing answer on promoting the load. + +**The blocker recorded when this was queued was real, and it is what made the item large.** The +change moves the drained numbers, so every drained figure already published measured a different +piece of code. Re-measured on the same host, three whole-probe invocations, committed as a capture +at `captures/2026-09-16-drained-handshake/` with the summarising script beside the raw runs so the +derivation can be checked rather than trusted. + +**The figures are amended rather than replaced**, because this is new data and not a correction: the +earlier capture remains what the earlier instrument measured, and both are labelled with the code +that produced them. The finding survived the re-measurement -- every layout median still sits inside +the same-code control band in the drained regime -- which is worth stating precisely because it was +not guaranteed: the drained conclusion did not depend on the window it had been measured through. diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index b17af814f..c9af3e494 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1006,6 +1006,18 @@ what "no difference" looks like on this host: | isolated | median 0.94-1.05x, observed 0.69-1.12x | | drained | median 0.98-1.07x, observed 0.68-1.27x | +**Amended 2026-09-16: the drained row measures a probe that no longer exists.** +`M4.3` added a readiness handshake, so producers now hold until the consumer +announces that it is draining rather than starting the moment the gate releases. +The row above was taken before that and is kept as what the earlier instrument +measured. A capture taken after it, on the same host, is in +[captures/2026-09-16-drained-handshake/](captures/2026-09-16-drained-handshake/README.md); +its control span is in that capture's `summary.txt` rather than restated here. +The two are not a like-for-like comparison of dispersion -- the row above spans +seven runs and the new capture three -- so the amendment records that the +measurement changed, not that the spread narrowed. The isolated row is +unaffected: those timers have no consumer, and so no handshake. + So a ratio inside roughly 0.9-1.1x is indistinguishable from zero effect here, and at sixteen and thirty-two producers the control alone wanders past 1.12x. @@ -1043,6 +1055,17 @@ In the drained regime nothing separates at all -- every u64 layout *and* the 128-bit word sit inside the control band at every producer count (the widest median is 1.13x at one producer, against a control that reaches 1.27x). +**Amended 2026-09-16: re-measured after the `M4.3` handshake, and the finding +stands.** That paragraph was taken before producers held for the consumer, so it +describes a drained regime whose opening was briefly undrained. Re-measured on +the same host without that window, every layout median still sits inside the +same-code control band -- the figures, and the check itself, are in +[captures/2026-09-16-drained-handshake/](captures/2026-09-16-drained-handshake/README.md) +rather than retyped here. What the re-measurement establishes is narrow and +worth stating plainly: the drained conclusion did not depend on the window it +was measured through. It says nothing about the isolated regime, which the +handshake does not touch. + **Widening the word is the one effect this probe establishes.** At sixteen and thirty-two producers the isolated 128-bit rows fall outside the same-code control band; the `u64` re-apportionments do not, at any producer count. diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md new file mode 100644 index 000000000..44acde42f --- /dev/null +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md @@ -0,0 +1,43 @@ +# Queue contention, drained regime, after the M4.3 readiness handshake + +Three whole-probe invocations taken to answer one question: does closing the +undrained opening at the start of each drained run change what the drained rows +say? + +**This is a second capture, not a replacement for the first.** The figures in +[DESIGN-NOTES.md](../../DESIGN-NOTES.md) that predate `M4.3` measured a probe +whose producers could begin pushing before the consumer reached its first `pop`. +These measured a probe where they cannot. Both are real measurements; they are +measurements of two different pieces of code, so they are kept side by side and +each is labelled with the instrument that produced it. + +## Attribution + +| | | +|---|---| +| Host | `x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16]` | +| Profile | release | +| Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | +| Runs | 3 whole-probe invocations, in [run1.txt](run1.txt), [run2.txt](run2.txt), [run3.txt](run3.txt) | +| Instrument | `probe-queue-contention`, built from `04e6d825` (the commit that added the handshake) | +| Taken | 2026-09-16 UTC-04:00 | + +The host is the same machine as the capture the crate README carries, so the two +are comparable; nothing here says anything about any other hardware, and the +banner's `numa[16]` is a single node holding all sixteen processors. + +## Reading it + +[summary.txt](summary.txt) is the output of [summarise.js](summarise.js) over the +three runs, regenerated with: + +``` +node summarise.js run1.txt run2.txt run3.txt +``` + +It is committed so the derivation can be checked rather than taken on trust, and +so nothing downstream has to retype a figure. The script derives two things the +runs do not state individually: the across-run median per producer count, and +the same-code control span -- which is a relation *between* two tables, since +`reserving_mpsc` in the comparison table and `32/32` in the layout table are the +same configuration measured twice in the same run. diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run1.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run1.txt new file mode 100644 index 000000000..2942ebb43 --- /dev/null +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run1.txt @@ -0,0 +1,209 @@ +host: x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16] +== how does the array queue's push path scale with producer count? == + +processors available to this process: 16 + +profile: release +sampling: 50000 pushes per producer, median of 5 repetitions, one untimed warmup pass + +-- isolated: producers only, capacity large enough that nothing is refused -- +shape producers ns/op ops/sec refusals ns/op range spread +baseline_fetch_add 1 2.7 373692078 0 2.3-3.2 1.40x +slotwise_mpsc 1 5.9 168350168 0 5.8-6.6 1.14x +reserving_mpsc 1 5.6 180050414 0 5.4-6.2 1.15x +permit_mpsc 1 7.9 126678490 0 7.9-8.1 1.03x +reserving(32/32) 1 5.4 184094256 0 5.4-5.6 1.04x +reserving(16/48) 1 5.4 185185185 0 5.4-8.5 1.58x +reserving(8/56) 1 5.7 176740898 0 5.4-5.9 1.09x +reserving(64/64) 1 7.4 134372481 0 7.4-7.5 1.00x +baseline_fetch_add 2 10.4 96061479 0 8.1-11.9 1.47x +slotwise_mpsc 2 47.6 21022536 0 30.1-56.3 1.87x +reserving_mpsc 2 34.6 28920959 0 18.4-37.4 2.04x +permit_mpsc 2 42.6 23472525 0 18.8-45.3 2.41x +reserving(32/32) 2 31.3 31933578 0 23.5-34.0 1.45x +reserving(16/48) 2 34.9 28687819 0 29.6-70.3 2.37x +reserving(8/56) 2 28.9 34622442 0 24.0-34.6 1.44x +reserving(64/64) 2 38.0 26322024 0 30.5-41.6 1.37x +baseline_fetch_add 4 14.3 70143443 0 13.0-14.6 1.13x +slotwise_mpsc 4 89.1 11224856 0 87.1-93.9 1.08x +reserving_mpsc 4 39.3 25432350 0 33.6-41.5 1.24x +permit_mpsc 4 30.4 32896360 0 30.0-31.8 1.06x +reserving(32/32) 4 33.1 30235687 0 32.5-35.7 1.10x +reserving(16/48) 4 34.5 28970812 0 31.2-38.1 1.22x +reserving(8/56) 4 36.9 27117541 0 35.5-37.6 1.06x +reserving(64/64) 4 51.9 19270236 0 46.3-68.9 1.49x +baseline_fetch_add 8 15.1 66408779 0 14.2-15.3 1.08x +slotwise_mpsc 8 134.2 7454064 0 129.4-142.6 1.10x +reserving_mpsc 8 38.0 26332074 0 36.4-47.8 1.31x +permit_mpsc 8 24.7 40512073 0 22.2-26.8 1.21x +reserving(32/32) 8 38.8 25754610 0 38.4-42.5 1.11x +reserving(16/48) 8 44.6 22409717 0 40.6-49.1 1.21x +reserving(8/56) 8 45.9 21773684 0 43.0-56.2 1.31x +reserving(64/64) 8 76.0 13152703 0 72.8-97.8 1.34x +baseline_fetch_add 16 14.5 68827862 0 14.4-14.7 1.02x +slotwise_mpsc 16 217.5 4598616 0 154.9-225.3 1.45x +reserving_mpsc 16 47.7 20975684 0 44.2-52.6 1.19x +permit_mpsc 16 23.4 42767255 0 20.9-24.1 1.15x +reserving(32/32) 16 45.1 22166252 0 43.2-49.0 1.13x +reserving(16/48) 16 62.5 16000704 0 55.0-67.6 1.23x +reserving(8/56) 16 55.0 18174672 0 48.1-62.5 1.30x +reserving(64/64) 16 154.4 6476139 0 142.2-184.2 1.30x +baseline_fetch_add 32 15.1 66355074 0 14.9-15.1 1.02x +slotwise_mpsc 32 225.0 4444205 0 206.1-243.2 1.18x +reserving_mpsc 32 49.1 20363540 0 48.6-53.8 1.11x +permit_mpsc 32 22.0 45390972 0 20.7-34.0 1.65x +reserving(32/32) 32 48.0 20817124 0 45.3-50.9 1.12x +reserving(16/48) 32 62.5 16006979 0 59.9-67.0 1.12x +reserving(8/56) 32 65.1 15367209 0 60.2-68.5 1.14x +reserving(64/64) 32 214.0 4673929 0 182.1-219.4 1.20x + +-- drained: a consumer popping continuously, capacity 1024 -- +shape producers ns/op ops/sec refusals ns/op range spread +slotwise_mpsc 1 10.6 94215187 35 9.9-14.1 1.42x +reserving_mpsc 1 25.4 39373179 461 24.9-26.7 1.07x +permit_mpsc 1 61.2 16327064 2663 60.2-63.9 1.06x +reserving(32/32) 1 25.5 39227993 1041 20.5-25.7 1.25x +reserving(16/48) 1 27.4 36531015 2158 26.3-28.3 1.07x +reserving(8/56) 1 27.1 36859565 2543 26.4-28.3 1.07x +reserving(64/64) 1 27.4 36555052 205 25.6-29.1 1.14x +slotwise_mpsc 2 57.7 17343045 2045 36.9-74.8 2.02x +reserving_mpsc 2 61.8 16186206 166 55.7-70.5 1.27x +permit_mpsc 2 53.5 18704175 2913 51.8-58.4 1.13x +reserving(32/32) 2 65.9 15166222 2329 63.3-69.8 1.10x +reserving(16/48) 2 64.2 15565171 599 57.0-69.6 1.22x +reserving(8/56) 2 67.9 14737307 1300 67.5-70.7 1.05x +reserving(64/64) 2 70.4 14202125 113 66.4-72.6 1.09x +slotwise_mpsc 4 99.4 10058692 2496 95.9-112.1 1.17x +reserving_mpsc 4 90.9 11001282 0 86.3-101.2 1.17x +permit_mpsc 4 82.4 12137174 88978 59.7-84.6 1.42x +reserving(32/32) 4 83.7 11949216 145 81.8-93.1 1.14x +reserving(16/48) 4 95.6 10461017 0 93.7-100.0 1.07x +reserving(8/56) 4 95.4 10482785 0 95.0-99.6 1.05x +reserving(64/64) 4 106.2 9416462 88 100.2-113.8 1.14x +slotwise_mpsc 8 150.0 6664834 1058 145.8-159.1 1.09x +reserving_mpsc 8 154.2 6485168 0 145.0-161.2 1.11x +permit_mpsc 8 135.6 7374645 532659 121.4-147.0 1.21x +reserving(32/32) 8 156.4 6394721 212 143.7-163.6 1.14x +reserving(16/48) 8 160.3 6240220 75970 150.4-164.9 1.10x +reserving(8/56) 8 153.1 6533471 11751 141.3-161.8 1.15x +reserving(64/64) 8 166.4 6011268 1395 149.8-175.9 1.17x +slotwise_mpsc 16 340.3 2938673 2850654 306.4-526.9 1.72x +reserving_mpsc 16 330.7 3023617 3732261 261.5-414.1 1.58x +permit_mpsc 16 245.4 4074334 2735377 177.2-317.7 1.79x +reserving(32/32) 16 264.0 3788550 1493570 221.9-332.3 1.50x +reserving(16/48) 16 291.5 3430305 3649382 274.0-427.6 1.56x +reserving(8/56) 16 266.1 3757414 1448525 251.7-315.0 1.25x +reserving(64/64) 16 345.7 2893058 3598613 297.3-403.2 1.36x +slotwise_mpsc 32 601.6 1662156 19050487 495.9-707.5 1.43x +reserving_mpsc 32 741.1 1349358 29947662 546.0-1040.8 1.91x +permit_mpsc 32 513.0 1949410 12629644 439.9-557.0 1.27x +reserving(32/32) 32 705.4 1417609 28232093 603.7-812.8 1.35x +reserving(16/48) 32 588.6 1698996 17387314 518.8-724.3 1.40x +reserving(8/56) 32 560.8 1783052 16742286 541.7-677.7 1.25x +reserving(64/64) 32 716.7 1395263 22491959 504.0-755.6 1.50x + +interpretation: + + 1. push-path scaling with producer count (isolated regime) + + producers slotwise reserving permit atomic floor + 1 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] + 2 0.12x [0.10-0.22] 0.16x [0.14-0.34] 0.19x [0.17-0.43] 0.26x [0.19-0.40] + 4 0.07x [0.06-0.08] 0.14x [0.13-0.19] 0.26x [0.25-0.27] 0.19x [0.16-0.25] + 8 0.04x [0.04-0.05] 0.15x [0.11-0.17] 0.32x [0.29-0.37] 0.18x [0.15-0.23] + 16 0.03x [0.03-0.04] 0.12x [0.10-0.14] 0.34x [0.33-0.39] 0.18x [0.16-0.22] + 32 0.03x [0.02-0.03] 0.11x [0.10-0.13] 0.36x [0.23-0.39] 0.18x [0.15-0.22] + + Read as: throughput at N producers divided by throughput at one. + 1.00 means N threads together push no faster than one did. + The atomic floor is the cheapest possible contended operation, + so it says how much of any curve is the queue and how much is + simply what this processor does to a fought-over cache line. + + 2. reserving vs slotwise, drained (where `head` is written) + + The ratio is the WHOLE push path of two different shapes, not the + price of reserving's extra `head` load on its own: they use + different claim protocols, slot metadata and retry behaviour. This + regime is where that load is at its most expensive -- but the + ratio still does not isolate it, or bound it either way. + + producers slotwise reserving reserving/slotwise permit permit/reserving + ns/op ns/op ratio [bound] ns/op ratio [bound] + 1 10.6 25.4 2.39x [1.77-2.70] 61.2 2.41x [2.26-2.57] + 2 57.7 61.8 1.07x [0.74-1.91] 53.5 0.87x [0.73-1.05] + 4 99.4 90.9 0.91x [0.77-1.06] 82.4 0.91x [0.59-0.98] + 8 150.0 154.2 1.03x [0.91-1.11] 135.6 0.88x [0.75-1.01] + 16 340.3 330.7 0.97x [0.50-1.35] 245.4 0.74x [0.43-1.22] + 32 601.6 741.1 1.23x [0.77-2.10] 513.0 0.69x [0.42-1.02] + + `reserving_mpsc` reads the consumer's position on every push and + `slotwise_mpsc` does not. This regime is where that read is at its + most expensive, because a consumer is writing the line being read + -- but the ratio does not decompose. It is an END-TO-END + comparison of two shapes: they also differ in claim protocol, + slot metadata and retry behaviour, and those differences are not + ordered. So this ratio neither isolates the read nor bounds it. + + `permit_mpsc` is experimental and is the candidate replacement + for `reserving_mpsc`: it removes that read entirely, and with it + the stale room decision behind SH-14.1, by making admission a + read-modify-write on a permit count instead. The last column is + the trade -- below 1.00 and the safer claim is also the cheaper + one; above 1.00 and closing the hole costs throughput. + + 3. claim-word layout + + 4 apportionments of reserving_mpsc's claim word, measured on + the shipping type itself rather than on a stand-in. 32/32 is the + default; 16/48 and 8/56 are the same u64 exchange with the bits + apportioned differently; 64/64 is a u128 exchange (cmpxchg16b on + x86-64, ldxp/stxp on aarch64), measured only where that is native. + The three u64 rows issue the same instruction and differ only in + shift and mask constants, so there is no structural reason for one + to be slower -- but these rows time the WHOLE push path, so a + difference between them is not thereby noise. Read it against a + control before calling it either way: the reserving_mpsc row and + the 32/32 row above are the same code, so the gap between them is + what 'no difference' looks like on this host -- which across seven + runs was not zero, and was wide enough to swallow the layout rows. + 64/64 vs 32/32 is the double-width layout's effect on the whole + push path -- what moving the recurrence to 2^64 costs, against + 8/56 moving it to 2^56. Both defer the recurrence rather than + removing it. Not the exchange in isolation. + + -- isolated -- + producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs + 1 5.4 5.4 5.7 7.4 0.99x [0.95-1.56] 1.04x [0.96-1.09] 1.37x [1.32-1.38] + 2 31.3 34.9 28.9 38.0 1.11x [0.87-3.00] 0.92x [0.71-1.47] 1.21x [0.90-1.77] + 4 33.1 34.5 36.9 51.9 1.04x [0.87-1.17] 1.11x [0.99-1.16] 1.57x [1.29-2.12] + 8 38.8 44.6 45.9 76.0 1.15x [0.95-1.28] 1.18x [1.01-1.47] 1.96x [1.71-2.55] + 16 45.1 62.5 55.0 154.4 1.39x [1.12-1.56] 1.22x [0.98-1.45] 3.42x [2.90-4.26] + 32 48.0 62.5 65.1 214.0 1.30x [1.18-1.48] 1.35x [1.18-1.51] 4.45x [3.58-4.84] + + -- drained -- + producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs + 1 25.5 27.4 27.1 27.4 1.07x [1.02-1.38] 1.06x [1.02-1.38] 1.07x [0.99-1.42] + 2 65.9 64.2 67.9 70.4 0.97x [0.82-1.10] 1.03x [0.97-1.12] 1.07x [0.95-1.15] + 4 83.7 95.6 95.4 106.2 1.14x [1.01-1.22] 1.14x [1.02-1.22] 1.27x [1.08-1.39] + 8 156.4 160.3 153.1 166.4 1.02x [0.92-1.15] 0.98x [0.86-1.13] 1.06x [0.92-1.22] + 16 264.0 291.5 266.1 345.7 1.10x [0.82-1.93] 1.01x [0.76-1.42] 1.31x [0.89-1.82] + 32 705.4 588.6 560.8 716.7 0.83x [0.64-1.20] 0.80x [0.67-1.12] 1.02x [0.62-1.25] + + the 32/32 row and the reserving_mpsc row above are the same + configuration run twice, so the gap between them is this host's + same-code control: whatever it shows is dispersion, not a + difference between shapes. Do not read it as noise that can be + discounted -- its width is an open question about this + instrument. They + are no longer a control against a duplicated implementation: the + shipping type takes the layout as a parameter, so there is nothing + left that could drift away from what callers actually run. + + CAUTION: the drained regime has ONE consumer, because that is what + MPSC means. At high producer counts it is expected to become + consumer-bound, and a plateau there says nothing about the claim. + The refusal counts above are what make that visible: a run with + many refusals met a full queue often, so the consumer is one term + in what it measured. That does not rule the tail out -- both can + bind at once, and these counts do not separate them. diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run2.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run2.txt new file mode 100644 index 000000000..d5819f18d --- /dev/null +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run2.txt @@ -0,0 +1,209 @@ +host: x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16] +== how does the array queue's push path scale with producer count? == + +processors available to this process: 16 + +profile: release +sampling: 50000 pushes per producer, median of 5 repetitions, one untimed warmup pass + +-- isolated: producers only, capacity large enough that nothing is refused -- +shape producers ns/op ops/sec refusals ns/op range spread +baseline_fetch_add 1 2.3 432525952 0 1.9-2.5 1.26x +slotwise_mpsc 1 6.0 167954316 0 5.9-6.0 1.02x +reserving_mpsc 1 5.4 184638109 0 5.4-5.7 1.05x +permit_mpsc 1 8.2 121536218 0 8.0-9.4 1.18x +reserving(32/32) 1 5.4 184638109 0 5.4-5.8 1.07x +reserving(16/48) 1 5.5 181686047 0 5.4-5.6 1.04x +reserving(8/56) 1 5.6 179533214 0 5.4-6.0 1.11x +reserving(64/64) 1 7.4 134408602 0 7.4-7.7 1.04x +baseline_fetch_add 2 11.2 89182199 0 6.6-13.5 2.06x +slotwise_mpsc 2 59.4 16830767 0 49.1-68.5 1.40x +reserving_mpsc 2 35.3 28317381 0 33.9-37.0 1.09x +permit_mpsc 2 40.9 24465430 0 26.8-44.2 1.65x +reserving(32/32) 2 32.5 30809995 0 24.5-34.6 1.41x +reserving(16/48) 2 34.9 28624589 0 25.2-35.9 1.42x +reserving(8/56) 2 33.2 30080616 0 31.3-35.1 1.12x +reserving(64/64) 2 38.6 25883936 0 34.6-39.2 1.13x +baseline_fetch_add 4 15.6 64094347 0 14.4-16.3 1.13x +slotwise_mpsc 4 86.3 11588493 0 75.5-88.8 1.18x +reserving_mpsc 4 35.0 28567348 0 29.3-40.5 1.38x +permit_mpsc 4 31.6 31670124 0 29.4-32.1 1.09x +reserving(32/32) 4 38.9 25697692 0 36.0-41.0 1.14x +reserving(16/48) 4 37.9 26380004 0 36.4-38.7 1.06x +reserving(8/56) 4 35.5 28167824 0 34.0-40.8 1.20x +reserving(64/64) 4 47.2 21202837 0 41.4-54.4 1.31x +baseline_fetch_add 8 15.3 65553352 0 14.1-16.9 1.20x +slotwise_mpsc 8 133.8 7475266 0 123.1-140.3 1.14x +reserving_mpsc 8 39.6 25279656 0 37.8-47.4 1.26x +permit_mpsc 8 25.5 39251860 0 23.7-27.1 1.14x +reserving(32/32) 8 42.4 23611496 0 36.4-43.8 1.20x +reserving(16/48) 8 53.3 18778285 0 45.4-55.9 1.23x +reserving(8/56) 8 37.6 26602289 0 33.4-47.2 1.41x +reserving(64/64) 8 71.6 13960777 0 49.6-98.2 1.98x +baseline_fetch_add 16 14.9 67111279 0 14.5-15.2 1.05x +slotwise_mpsc 16 198.5 5037790 0 188.8-214.9 1.14x +reserving_mpsc 16 49.7 20128673 0 49.4-54.0 1.09x +permit_mpsc 16 20.7 48241012 0 20.1-23.6 1.18x +reserving(32/32) 16 47.6 20992747 0 44.0-47.7 1.09x +reserving(16/48) 16 61.0 16405814 0 57.7-64.9 1.13x +reserving(8/56) 16 61.4 16299586 0 53.0-65.9 1.24x +reserving(64/64) 16 166.7 5998036 0 124.4-192.9 1.55x +baseline_fetch_add 32 15.3 65501290 0 14.9-15.6 1.04x +slotwise_mpsc 32 225.5 4435428 0 218.8-230.0 1.05x +reserving_mpsc 32 51.5 19404288 0 48.4-53.4 1.10x +permit_mpsc 32 21.4 46727607 0 21.2-22.1 1.04x +reserving(32/32) 32 50.0 20014611 0 46.1-50.1 1.09x +reserving(16/48) 32 62.9 15899104 0 51.8-67.3 1.30x +reserving(8/56) 32 53.2 18804150 0 51.1-60.3 1.18x +reserving(64/64) 32 201.4 4964755 0 168.8-209.4 1.24x + +-- drained: a consumer popping continuously, capacity 1024 -- +shape producers ns/op ops/sec refusals ns/op range spread +slotwise_mpsc 1 9.8 101729400 9 9.1-11.1 1.22x +reserving_mpsc 1 25.2 39610235 1026 23.8-27.8 1.17x +permit_mpsc 1 61.0 16394518 543 59.6-62.9 1.05x +reserving(32/32) 1 26.3 37962190 888 25.5-28.3 1.11x +reserving(16/48) 1 28.0 35670971 315 26.7-29.9 1.12x +reserving(8/56) 1 27.9 35888602 389 25.3-30.6 1.21x +reserving(64/64) 1 29.8 33518804 1321 26.6-31.2 1.17x +slotwise_mpsc 2 77.5 12906723 372 20.6-80.0 3.88x +reserving_mpsc 2 69.2 14457344 204 62.3-69.9 1.12x +permit_mpsc 2 54.3 18414511 2359 52.5-55.3 1.05x +reserving(32/32) 2 67.6 14798810 426 63.8-70.2 1.10x +reserving(16/48) 2 70.0 14295721 0 65.3-72.2 1.11x +reserving(8/56) 2 67.2 14890481 4459 65.3-71.5 1.10x +reserving(64/64) 2 72.0 13888696 1013 69.5-73.8 1.06x +slotwise_mpsc 4 104.9 9529573 32140 90.0-114.0 1.27x +reserving_mpsc 4 94.3 10607266 6854 87.9-103.6 1.18x +permit_mpsc 4 80.3 12450432 85539 51.0-83.3 1.63x +reserving(32/32) 4 94.0 10641298 4458 84.7-102.0 1.20x +reserving(16/48) 4 95.6 10458063 0 91.5-101.4 1.11x +reserving(8/56) 4 99.4 10061678 564 92.8-102.2 1.10x +reserving(64/64) 4 104.3 9584511 105 94.4-112.9 1.20x +slotwise_mpsc 8 152.0 6577065 4472 150.8-159.0 1.05x +reserving_mpsc 8 159.6 6264859 2131 150.7-161.0 1.07x +permit_mpsc 8 142.6 7012992 585672 134.8-148.4 1.10x +reserving(32/32) 8 160.5 6230617 5597 154.7-178.9 1.16x +reserving(16/48) 8 161.7 6183623 2970 156.1-169.5 1.09x +reserving(8/56) 8 157.0 6369701 645 143.2-162.1 1.13x +reserving(64/64) 8 175.2 5708675 1471 165.2-178.0 1.08x +slotwise_mpsc 16 319.5 3130055 2493998 214.1-380.0 1.78x +reserving_mpsc 16 275.5 3629762 2111325 266.6-365.4 1.37x +permit_mpsc 16 291.4 3432079 3319000 235.6-312.6 1.33x +reserving(32/32) 16 299.9 3334441 2633072 248.6-348.8 1.40x +reserving(16/48) 16 285.1 3507777 1482100 225.9-295.6 1.31x +reserving(8/56) 16 321.4 3111441 2155304 222.5-353.7 1.59x +reserving(64/64) 16 305.5 3273403 2010188 267.7-348.8 1.30x +slotwise_mpsc 32 550.8 1815636 15636391 518.0-664.1 1.28x +reserving_mpsc 32 742.1 1347448 30259881 657.0-804.2 1.22x +permit_mpsc 32 500.8 1996881 11882456 367.0-540.3 1.47x +reserving(32/32) 32 687.7 1454211 26050632 495.1-989.3 2.00x +reserving(16/48) 32 789.2 1267080 29833589 614.3-1050.1 1.71x +reserving(8/56) 32 661.8 1511090 21873245 615.6-713.6 1.16x +reserving(64/64) 32 546.9 1828526 14442989 399.8-1187.3 2.97x + +interpretation: + + 1. push-path scaling with producer count (isolated regime) + + producers slotwise reserving permit atomic floor + 1 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] + 2 0.10x [0.09-0.12] 0.15x [0.15-0.17] 0.20x [0.18-0.35] 0.21x [0.14-0.37] + 4 0.07x [0.07-0.08] 0.15x [0.13-0.19] 0.26x [0.25-0.32] 0.15x [0.12-0.17] + 8 0.04x [0.04-0.05] 0.14x [0.11-0.15] 0.32x [0.29-0.40] 0.15x [0.12-0.17] + 16 0.03x [0.03-0.03] 0.11x [0.10-0.11] 0.40x [0.34-0.47] 0.16x [0.13-0.17] + 32 0.03x [0.03-0.03] 0.11x [0.10-0.12] 0.38x [0.36-0.44] 0.15x [0.13-0.16] + + Read as: throughput at N producers divided by throughput at one. + 1.00 means N threads together push no faster than one did. + The atomic floor is the cheapest possible contended operation, + so it says how much of any curve is the queue and how much is + simply what this processor does to a fought-over cache line. + + 2. reserving vs slotwise, drained (where `head` is written) + + The ratio is the WHOLE push path of two different shapes, not the + price of reserving's extra `head` load on its own: they use + different claim protocols, slot metadata and retry behaviour. This + regime is where that load is at its most expensive -- but the + ratio still does not isolate it, or bound it either way. + + producers slotwise reserving reserving/slotwise permit permit/reserving + ns/op ns/op ratio [bound] ns/op ratio [bound] + 1 9.8 25.2 2.57x [2.15-3.06] 61.0 2.42x [2.15-2.64] + 2 77.5 69.2 0.89x [0.78-3.38] 54.3 0.79x [0.75-0.89] + 4 104.9 94.3 0.90x [0.77-1.15] 80.3 0.85x [0.49-0.95] + 8 152.0 159.6 1.05x [0.95-1.07] 142.6 0.89x [0.84-0.98] + 16 319.5 275.5 0.86x [0.70-1.71] 291.4 1.06x [0.64-1.17] + 32 550.8 742.1 1.35x [0.99-1.55] 500.8 0.67x [0.46-0.82] + + `reserving_mpsc` reads the consumer's position on every push and + `slotwise_mpsc` does not. This regime is where that read is at its + most expensive, because a consumer is writing the line being read + -- but the ratio does not decompose. It is an END-TO-END + comparison of two shapes: they also differ in claim protocol, + slot metadata and retry behaviour, and those differences are not + ordered. So this ratio neither isolates the read nor bounds it. + + `permit_mpsc` is experimental and is the candidate replacement + for `reserving_mpsc`: it removes that read entirely, and with it + the stale room decision behind SH-14.1, by making admission a + read-modify-write on a permit count instead. The last column is + the trade -- below 1.00 and the safer claim is also the cheaper + one; above 1.00 and closing the hole costs throughput. + + 3. claim-word layout + + 4 apportionments of reserving_mpsc's claim word, measured on + the shipping type itself rather than on a stand-in. 32/32 is the + default; 16/48 and 8/56 are the same u64 exchange with the bits + apportioned differently; 64/64 is a u128 exchange (cmpxchg16b on + x86-64, ldxp/stxp on aarch64), measured only where that is native. + The three u64 rows issue the same instruction and differ only in + shift and mask constants, so there is no structural reason for one + to be slower -- but these rows time the WHOLE push path, so a + difference between them is not thereby noise. Read it against a + control before calling it either way: the reserving_mpsc row and + the 32/32 row above are the same code, so the gap between them is + what 'no difference' looks like on this host -- which across seven + runs was not zero, and was wide enough to swallow the layout rows. + 64/64 vs 32/32 is the double-width layout's effect on the whole + push path -- what moving the recurrence to 2^64 costs, against + 8/56 moving it to 2^56. Both defer the recurrence rather than + removing it. Not the exchange in isolation. + + -- isolated -- + producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs + 1 5.4 5.5 5.6 7.4 1.02x [0.93-1.03] 1.03x [0.93-1.11] 1.37x [1.29-1.43] + 2 32.5 34.9 33.2 38.6 1.08x [0.73-1.47] 1.02x [0.91-1.43] 1.19x [1.00-1.60] + 4 38.9 37.9 35.5 47.2 0.97x [0.89-1.08] 0.91x [0.83-1.13] 1.21x [1.01-1.51] + 8 42.4 53.3 37.6 71.6 1.26x [1.04-1.54] 0.89x [0.76-1.30] 1.69x [1.13-2.70] + 16 47.6 61.0 61.4 166.7 1.28x [1.21-1.48] 1.29x [1.11-1.50] 3.50x [2.61-4.39] + 32 50.0 62.9 53.2 201.4 1.26x [1.03-1.46] 1.06x [1.02-1.31] 4.03x [3.37-4.54] + + -- drained -- + producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs + 1 26.3 28.0 27.9 29.8 1.06x [0.95-1.17] 1.06x [0.90-1.20] 1.13x [0.94-1.23] + 2 67.6 70.0 67.2 72.0 1.04x [0.93-1.13] 0.99x [0.93-1.12] 1.07x [0.99-1.16] + 4 94.0 95.6 99.4 104.3 1.02x [0.90-1.20] 1.06x [0.91-1.21] 1.11x [0.92-1.33] + 8 160.5 161.7 157.0 175.2 1.01x [0.87-1.10] 0.98x [0.80-1.05] 1.09x [0.92-1.15] + 16 299.9 285.1 321.4 305.5 0.95x [0.65-1.19] 1.07x [0.64-1.42] 1.02x [0.77-1.40] + 32 687.7 789.2 661.8 546.9 1.15x [0.62-2.12] 0.96x [0.62-1.44] 0.80x [0.40-2.40] + + the 32/32 row and the reserving_mpsc row above are the same + configuration run twice, so the gap between them is this host's + same-code control: whatever it shows is dispersion, not a + difference between shapes. Do not read it as noise that can be + discounted -- its width is an open question about this + instrument. They + are no longer a control against a duplicated implementation: the + shipping type takes the layout as a parameter, so there is nothing + left that could drift away from what callers actually run. + + CAUTION: the drained regime has ONE consumer, because that is what + MPSC means. At high producer counts it is expected to become + consumer-bound, and a plateau there says nothing about the claim. + The refusal counts above are what make that visible: a run with + many refusals met a full queue often, so the consumer is one term + in what it measured. That does not rule the tail out -- both can + bind at once, and these counts do not separate them. diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run3.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run3.txt new file mode 100644 index 000000000..9d688605d --- /dev/null +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run3.txt @@ -0,0 +1,209 @@ +host: x86_64 16p/8c smt+ L2[2,2,2,2,2,2,2,2] ec[0:16] numa[16] +== how does the array queue's push path scale with producer count? == + +processors available to this process: 16 + +profile: release +sampling: 50000 pushes per producer, median of 5 repetitions, one untimed warmup pass + +-- isolated: producers only, capacity large enough that nothing is refused -- +shape producers ns/op ops/sec refusals ns/op range spread +baseline_fetch_add 1 2.3 431778929 0 2.3-3.3 1.43x +slotwise_mpsc 1 6.1 163452109 0 6.0-6.4 1.07x +reserving_mpsc 1 5.4 183891136 0 5.4-5.7 1.05x +permit_mpsc 1 7.9 126742712 0 7.9-8.4 1.07x +reserving(32/32) 1 5.4 184979652 0 5.4-5.4 1.01x +reserving(16/48) 1 5.4 185253798 0 5.4-5.6 1.04x +reserving(8/56) 1 5.4 185459941 0 5.4-5.4 1.01x +reserving(64/64) 1 7.6 130855797 0 7.5-8.0 1.08x +baseline_fetch_add 2 11.7 85229694 0 9.9-11.9 1.20x +slotwise_mpsc 2 51.2 19515241 0 31.0-57.3 1.84x +reserving_mpsc 2 31.9 31332247 0 15.1-34.1 2.25x +permit_mpsc 2 41.0 24408103 0 37.6-41.8 1.11x +reserving(32/32) 2 34.8 28724902 0 32.6-36.6 1.12x +reserving(16/48) 2 34.4 29085832 0 31.0-36.8 1.19x +reserving(8/56) 2 30.9 32324800 0 27.0-35.1 1.30x +reserving(64/64) 2 38.2 26177325 0 23.8-39.1 1.64x +baseline_fetch_add 4 14.8 67553874 0 13.1-16.0 1.22x +slotwise_mpsc 4 90.2 11089487 0 83.8-92.7 1.11x +reserving_mpsc 4 34.7 28830907 0 32.8-36.9 1.13x +permit_mpsc 4 30.9 32372936 0 30.0-33.2 1.10x +reserving(32/32) 4 35.5 28170204 0 30.0-37.0 1.23x +reserving(16/48) 4 37.2 26877024 0 35.5-42.3 1.19x +reserving(8/56) 4 38.8 25759254 0 32.1-39.4 1.23x +reserving(64/64) 4 50.8 19699387 0 42.5-51.5 1.21x +baseline_fetch_add 8 15.2 65907630 0 14.3-15.8 1.11x +slotwise_mpsc 8 147.0 6801160 0 141.1-148.6 1.05x +reserving_mpsc 8 41.7 23998800 0 35.9-53.1 1.48x +permit_mpsc 8 25.1 39891097 0 25.0-25.8 1.03x +reserving(32/32) 8 41.9 23840744 0 39.7-45.4 1.14x +reserving(16/48) 8 42.5 23508254 0 37.4-47.4 1.27x +reserving(8/56) 8 34.8 28721395 0 33.1-47.8 1.44x +reserving(64/64) 8 79.3 12609148 0 77.3-88.4 1.14x +baseline_fetch_add 16 15.1 66413190 0 14.7-15.4 1.05x +slotwise_mpsc 16 208.7 4792399 0 189.7-231.5 1.22x +reserving_mpsc 16 50.3 19870840 0 45.8-52.7 1.15x +permit_mpsc 16 21.0 47640883 0 20.5-23.9 1.17x +reserving(32/32) 16 45.9 21766990 0 40.7-51.8 1.27x +reserving(16/48) 16 65.0 15374031 0 58.8-67.8 1.15x +reserving(8/56) 16 57.0 17551057 0 52.1-62.6 1.20x +reserving(64/64) 16 175.5 5697628 0 134.9-194.4 1.44x +baseline_fetch_add 32 15.0 66880405 0 14.7-15.5 1.05x +slotwise_mpsc 32 252.5 3960634 0 202.8-257.8 1.27x +reserving_mpsc 32 49.3 20274183 0 46.0-51.0 1.11x +permit_mpsc 32 22.4 44708598 0 22.0-22.6 1.03x +reserving(32/32) 32 49.0 20420588 0 47.8-51.7 1.08x +reserving(16/48) 32 63.1 15840816 0 61.9-72.4 1.17x +reserving(8/56) 32 63.8 15686075 0 61.5-69.6 1.13x +reserving(64/64) 32 173.8 5754768 0 152.3-195.8 1.29x + +-- drained: a consumer popping continuously, capacity 1024 -- +shape producers ns/op ops/sec refusals ns/op range spread +slotwise_mpsc 1 10.7 93861460 1482 9.7-11.1 1.15x +reserving_mpsc 1 25.2 39739310 243 24.6-31.0 1.26x +permit_mpsc 1 62.1 16091140 506 58.6-62.8 1.07x +reserving(32/32) 1 24.8 40397512 977 21.0-26.9 1.28x +reserving(16/48) 1 28.2 35463508 758 27.2-32.2 1.19x +reserving(8/56) 1 28.8 34693311 0 23.8-33.4 1.40x +reserving(64/64) 1 27.8 35955703 1114 27.1-33.7 1.25x +slotwise_mpsc 2 65.2 15331075 560 49.7-70.0 1.41x +reserving_mpsc 2 68.6 14582999 29 65.2-70.7 1.08x +permit_mpsc 2 52.5 19030944 585 50.8-56.8 1.12x +reserving(32/32) 2 67.3 14864142 308 65.5-70.5 1.08x +reserving(16/48) 2 66.9 14943662 0 62.8-68.5 1.09x +reserving(8/56) 2 66.3 15085914 756 65.5-69.8 1.06x +reserving(64/64) 2 68.4 14616250 791 46.7-70.9 1.52x +slotwise_mpsc 4 99.5 10048181 0 94.4-111.2 1.18x +reserving_mpsc 4 89.4 11188436 6870 78.3-98.7 1.26x +permit_mpsc 4 77.8 12851323 86633 68.9-80.4 1.17x +reserving(32/32) 4 96.7 10343615 185 94.9-101.9 1.07x +reserving(16/48) 4 93.7 10669853 15608 82.4-99.8 1.21x +reserving(8/56) 4 97.7 10234419 0 91.0-99.0 1.09x +reserving(64/64) 4 106.3 9406010 0 100.7-125.6 1.25x +slotwise_mpsc 8 149.5 6688057 4512 144.5-156.5 1.08x +reserving_mpsc 8 162.5 6153174 1594 155.1-164.2 1.06x +permit_mpsc 8 142.1 7036790 594273 122.2-152.3 1.25x +reserving(32/32) 8 162.5 6154926 1295 160.3-166.4 1.04x +reserving(16/48) 8 165.0 6060496 220 148.6-168.6 1.13x +reserving(8/56) 8 161.1 6205639 15873 150.5-169.0 1.12x +reserving(64/64) 8 172.1 5810212 2748 153.0-177.0 1.16x +slotwise_mpsc 16 249.0 4016470 664737 239.4-335.5 1.40x +reserving_mpsc 16 270.7 3694365 1613977 263.2-391.1 1.49x +permit_mpsc 16 265.0 3774174 2932676 209.5-315.4 1.51x +reserving(32/32) 16 264.7 3777680 1609361 213.5-333.8 1.56x +reserving(16/48) 16 266.3 3755021 1371904 243.7-312.5 1.28x +reserving(8/56) 16 278.9 3585232 1373700 260.9-307.7 1.18x +reserving(64/64) 16 241.0 4148807 570234 213.1-288.0 1.35x +slotwise_mpsc 32 537.5 1860401 15048333 484.4-658.6 1.36x +reserving_mpsc 32 609.1 1641797 22486824 484.0-802.9 1.66x +permit_mpsc 32 491.5 2034595 11829136 405.7-647.8 1.60x +reserving(32/32) 32 595.0 1680775 21516893 527.5-787.1 1.49x +reserving(16/48) 32 573.8 1742909 19007483 519.6-797.0 1.53x +reserving(8/56) 32 628.4 1591406 20968836 569.1-889.8 1.56x +reserving(64/64) 32 654.3 1528245 19098846 451.5-670.6 1.49x + +interpretation: + + 1. push-path scaling with producer count (isolated regime) + + producers slotwise reserving permit atomic floor + 1 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] + 2 0.12x [0.10-0.21] 0.17x [0.16-0.38] 0.19x [0.19-0.22] 0.20x [0.19-0.33] + 4 0.07x [0.06-0.08] 0.16x [0.15-0.17] 0.26x [0.24-0.28] 0.16x [0.14-0.25] + 8 0.04x [0.04-0.05] 0.13x [0.10-0.16] 0.31x [0.31-0.34] 0.15x [0.15-0.23] + 16 0.03x [0.03-0.03] 0.11x [0.10-0.12] 0.38x [0.33-0.41] 0.15x [0.15-0.22] + 32 0.02x [0.02-0.03] 0.11x [0.11-0.12] 0.35x [0.35-0.38] 0.15x [0.15-0.22] + + Read as: throughput at N producers divided by throughput at one. + 1.00 means N threads together push no faster than one did. + The atomic floor is the cheapest possible contended operation, + so it says how much of any curve is the queue and how much is + simply what this processor does to a fought-over cache line. + + 2. reserving vs slotwise, drained (where `head` is written) + + The ratio is the WHOLE push path of two different shapes, not the + price of reserving's extra `head` load on its own: they use + different claim protocols, slot metadata and retry behaviour. This + regime is where that load is at its most expensive -- but the + ratio still does not isolate it, or bound it either way. + + producers slotwise reserving reserving/slotwise permit permit/reserving + ns/op ns/op ratio [bound] ns/op ratio [bound] + 1 10.7 25.2 2.36x [2.21-3.21] 62.1 2.47x [1.89-2.55] + 2 65.2 68.6 1.05x [0.93-1.42] 52.5 0.77x [0.72-0.87] + 4 99.5 89.4 0.90x [0.70-1.05] 77.8 0.87x [0.70-1.03] + 8 149.5 162.5 1.09x [0.99-1.14] 142.1 0.87x [0.74-0.98] + 16 249.0 270.7 1.09x [0.78-1.63] 265.0 0.98x [0.54-1.20] + 32 537.5 609.1 1.13x [0.73-1.66] 491.5 0.81x [0.51-1.34] + + `reserving_mpsc` reads the consumer's position on every push and + `slotwise_mpsc` does not. This regime is where that read is at its + most expensive, because a consumer is writing the line being read + -- but the ratio does not decompose. It is an END-TO-END + comparison of two shapes: they also differ in claim protocol, + slot metadata and retry behaviour, and those differences are not + ordered. So this ratio neither isolates the read nor bounds it. + + `permit_mpsc` is experimental and is the candidate replacement + for `reserving_mpsc`: it removes that read entirely, and with it + the stale room decision behind SH-14.1, by making admission a + read-modify-write on a permit count instead. The last column is + the trade -- below 1.00 and the safer claim is also the cheaper + one; above 1.00 and closing the hole costs throughput. + + 3. claim-word layout + + 4 apportionments of reserving_mpsc's claim word, measured on + the shipping type itself rather than on a stand-in. 32/32 is the + default; 16/48 and 8/56 are the same u64 exchange with the bits + apportioned differently; 64/64 is a u128 exchange (cmpxchg16b on + x86-64, ldxp/stxp on aarch64), measured only where that is native. + The three u64 rows issue the same instruction and differ only in + shift and mask constants, so there is no structural reason for one + to be slower -- but these rows time the WHOLE push path, so a + difference between them is not thereby noise. Read it against a + control before calling it either way: the reserving_mpsc row and + the 32/32 row above are the same code, so the gap between them is + what 'no difference' looks like on this host -- which across seven + runs was not zero, and was wide enough to swallow the layout rows. + 64/64 vs 32/32 is the double-width layout's effect on the whole + push path -- what moving the recurrence to 2^64 costs, against + 8/56 moving it to 2^56. Both defer the recurrence rather than + removing it. Not the exchange in isolation. + + -- isolated -- + producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs + 1 5.4 5.4 5.4 7.6 1.00x [0.99-1.04] 1.00x [0.99-1.01] 1.41x [1.37-1.49] + 2 34.8 34.4 30.9 38.2 0.99x [0.85-1.13] 0.89x [0.74-1.08] 1.10x [0.65-1.20] + 4 35.5 37.2 38.8 50.8 1.05x [0.96-1.41] 1.09x [0.87-1.31] 1.43x [1.15-1.72] + 8 41.9 42.5 34.8 79.3 1.01x [0.82-1.19] 0.83x [0.73-1.20] 1.89x [1.70-2.22] + 16 45.9 65.0 57.0 175.5 1.42x [1.14-1.67] 1.24x [1.01-1.54] 3.82x [2.61-4.78] + 32 49.0 63.1 63.8 173.8 1.29x [1.20-1.51] 1.30x [1.19-1.46] 3.55x [2.95-4.10] + + -- drained -- + producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs + 1 24.8 28.2 28.8 27.8 1.14x [1.01-1.54] 1.16x [0.89-1.59] 1.12x [1.01-1.61] + 2 67.3 66.9 66.3 68.4 0.99x [0.89-1.05] 0.99x [0.93-1.07] 1.02x [0.66-1.08] + 4 96.7 93.7 97.7 106.3 0.97x [0.81-1.05] 1.01x [0.89-1.04] 1.10x [0.99-1.32] + 8 162.5 165.0 161.1 172.1 1.02x [0.89-1.05] 0.99x [0.90-1.05] 1.06x [0.92-1.10] + 16 264.7 266.3 278.9 241.0 1.01x [0.73-1.46] 1.05x [0.78-1.44] 0.91x [0.64-1.35] + 32 595.0 573.8 628.4 654.3 0.96x [0.66-1.51] 1.06x [0.72-1.69] 1.10x [0.57-1.27] + + the 32/32 row and the reserving_mpsc row above are the same + configuration run twice, so the gap between them is this host's + same-code control: whatever it shows is dispersion, not a + difference between shapes. Do not read it as noise that can be + discounted -- its width is an open question about this + instrument. They + are no longer a control against a duplicated implementation: the + shipping type takes the layout as a parameter, so there is nothing + left that could drift away from what callers actually run. + + CAUTION: the drained regime has ONE consumer, because that is what + MPSC means. At high producer counts it is expected to become + consumer-bound, and a plateau there says nothing about the claim. + The refusal counts above are what make that visible: a run with + many refusals met a full queue often, so the consumer is one term + in what it measured. That does not rule the tail out -- both can + bind at once, and these counts do not separate them. diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js new file mode 100644 index 000000000..df8f1efea --- /dev/null +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -0,0 +1,94 @@ +// Summarise the drained tables of a queue-contention capture. +// +// Reads the probe's own report text rather than re-deriving anything: the +// medians and bounds are whatever the instrument printed. What this adds is the +// across-run median per producer count, and the same-code control span, which +// no single run states because it is a relation between two tables. + +const fs = require("fs"); + +const RATIO = /([0-9.]+)x \[([0-9.]+)-([0-9.]+)\]/g; + +function median(values) { + const sorted = [...values].sort((a, b) => a - b); + const middle = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 + ? (sorted[middle - 1] + sorted[middle]) / 2 + : sorted[middle]; +} + +// producers -> { narrowNanos, ratios: [16/48, 8/56, 64/64] } +function drainedLayout(lines) { + let start = -1; + lines.forEach((line, i) => { + if (line.includes("-- drained --")) start = i; + }); + const rows = new Map(); + for (const line of lines.slice(start + 2, start + 8)) { + const fields = line.trim().split(/\s+/); + const ratios = [...line.matchAll(RATIO)].map((m) => Number(m[1])); + rows.set(Number(fields[0]), { narrowNanos: Number(fields[1]), ratios }); + } + return rows; +} + +// producers -> reserving ns/op, from the comparison table +function drainedComparison(lines) { + const start = lines.findIndex((line) => line.includes("reserving/slotwise")); + const rows = new Map(); + for (const line of lines.slice(start + 2, start + 8)) { + const fields = line.trim().split(/\s+/); + rows.set(Number(fields[0]), Number(fields[2])); + } + return rows; +} + +const paths = process.argv.slice(2); +const layouts = []; +const controls = []; +for (const path of paths) { + const lines = fs.readFileSync(path, "utf8").split(/\r?\n/); + const layout = drainedLayout(lines); + const comparison = drainedComparison(lines); + layouts.push(layout); + // The same code measured twice in one run: `reserving_mpsc` in the comparison + // table against `32/32` in the layout table. + const control = new Map(); + for (const [producers, row] of layout) { + if (row.narrowNanos > 0) { + control.set(producers, comparison.get(producers) / row.narrowNanos); + } + } + controls.push(control); +} + +const producers = [...layouts[0].keys()].sort((a, b) => a - b); +console.log(`runs: ${paths.length}`); +console.log(""); +console.log("drained layout ratios vs 32/32, median across runs"); +console.log("producers 16/48 8/56 64/64"); +const medians = []; +for (const p of producers) { + const row = [0, 1, 2].map((column) => + median(layouts.map((layout) => layout.get(p).ratios[column])), + ); + medians.push(...row); + console.log( + `${String(p).padStart(9)} ` + row.map((m) => `${m.toFixed(2)}x`).join(" "), + ); +} + +const every = controls.flatMap((control) => [...control.values()]); +console.log(""); +console.log("same-code control (reserving_mpsc vs reserving 32/32), drained"); +console.log(` observations: ${every.length}`); +console.log( + ` span: ${Math.min(...every).toFixed(2)}x to ${Math.max(...every).toFixed(2)}x`, +); +console.log(` median: ${median(every).toFixed(2)}x`); +console.log(""); +const widest = Math.max(...medians); +console.log(`widest layout median: ${widest.toFixed(2)}x`); +console.log( + `inside the control band: ${Math.min(...every) <= widest && widest <= Math.max(...every)}`, +); diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt new file mode 100644 index 000000000..9195e911b --- /dev/null +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt @@ -0,0 +1,18 @@ +runs: 3 + +drained layout ratios vs 32/32, median across runs +producers 16/48 8/56 64/64 + 1 1.07x 1.06x 1.12x + 2 0.99x 0.99x 1.07x + 4 1.02x 1.06x 1.11x + 8 1.02x 0.98x 1.06x + 16 1.01x 1.05x 1.02x + 32 0.96x 0.96x 1.02x + +same-code control (reserving_mpsc vs reserving 32/32), drained + observations: 18 + span: 0.92x to 1.25x + median: 1.01x + +widest layout median: 1.12x +inside the control band: true From cfa936fc60e1084f5167411a7a5d92e34eab92b3 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 17:33:06 -0400 Subject: [PATCH 081/139] docs(probes): state the property, and pin the census that decided it Completed item: M2.16: Repair the garbled `Report` doc comment, and drop the two counts that had rotted beside it. `src/report.rs` opened its `Report` sink doc with a dangling fragment -- a title, a blank line, then "is arithmetic. Every renderer writes through ..." -- and further down repeated the bare word "signatures." after a sentence that already ended in it. **The history was recovered rather than guessed, and it is the argument.** `b5594860` wrote the passage with a full sentence and a count of 504 write sites. `3827dc32`, titled "Update comments in report.rs for better clarity", revised that count to 332 and, in the same edit, deleted the two lines carrying the sentence. The edit that maintained the census is the edit that broke the prose -- which is as direct a case for CONTRACT INTEGRITY rule 4 as this repository has produced. Fixed in both homes together, because fixing one alone creates a fresh disagreement: - `report.rs` now states the property instead of the count. `String` already implements `fmt::Write`, so a sink that does too leaves every write site untouched and moves only the renderer signatures. That is what makes the point and cannot rot. The lost sentence is restored and the duplicate removed. - `DESIGN-NOTES.md` keeps its numbers, because there they *are* the finding: the decision was taken by counting, and the passage contrasts M1's estimate of "upwards of 160" against what re-measuring found. They are now pinned as the census as it stood on the day of the decision rather than written in the present tense, and the passage no longer repeats the figure four times to make one argument. **A third home was found by sweeping and deliberately left alone.** `COMPLETED-CHECKLIST.md`'s archived `M1.1` restates 332 and 18, but it is pre-existing on main rather than authored here, it sits in an append-only archive dated by its own heading, and there the count is the whole of the record -- that entry exists to say the estimate was wrong and that re-measuring decided the question. The design note's new wording agrees with it rather than contradicting it. Taken into this branch because the design note is already one of its files, so the stale count was a latent finding inside its diff rather than work borrowed from elsewhere. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 19 +--------- .../COMPLETED-CHECKLIST.md | 35 +++++++++++++++++++ .../windows-platform-probes/DESIGN-NOTES.md | 34 +++++++++++------- crates/windows-platform-probes/src/report.rs | 20 +++++++---- 4 files changed, 72 insertions(+), 36 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 88292b9aa..f9141e447 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -323,21 +323,4 @@ IDs keep their M2 numbers, for the reason given under M4. - [x] **M2.14.2** -- Add to CONTRACT INTEGRITY rule 1 the one thing this branch learned that it does NOT already say. -> [completed 2026-09-13](COMPLETED-CHECKLIST.md#m2142) -- [ ] **M2.16** -- Repair the garbled `Report` doc comment, and drop the two counts that have already - rotted beside it. - - [src/report.rs](src/report.rs) opens its `Report` sink doc with a dangling fragment -- "A [`Report`] - a renderer can `writeln!` into directly." followed by a blank line and then "is arithmetic. Every - renderer writes through ..." -- so a sentence was lost in an edit, and "moves only 18 renderer - signatures." is followed by a bare repeat of the word "signatures." Introduced 2026-09-09 by - `b5594860` and `3827dc32`, both already on main; found while sweeping a count defect on the report - -oracle branch, where the file was out of scope to touch. - - Both surviving numbers in that passage are censuses that have since drifted. It claims **332 - `writeln!` sites**; measured now, 354. [DESIGN-NOTES.md](DESIGN-NOTES.md) restates the same 332, - so the two must be fixed together or they drift apart again. Replace them with the invariant the - passage is actually arguing -- that `String` already implements `fmt::Write`, so every existing - write site stands untouched and only the renderer signatures move -- which is what makes the point - and cannot rot. This is the same defect class as CONTRACT INTEGRITY rule 1 in - [.github/copilot-instructions.md](../../.github/copilot-instructions.md), which M2.14 exists to - make bite. +- [x] **M2.16** -- Repair the garbled `Report` doc comment, and drop the two counts that had rotted beside it. -> [completed 2026-09-16 UTC-04:00](COMPLETED-CHECKLIST.md#m216) diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 862de0de7..26446060e 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1202,3 +1202,38 @@ earlier capture remains what the earlier instrument measured, and both are label that produced them. The finding survived the re-measurement -- every layout median still sits inside the same-code control band in the drained regime -- which is worth stating precisely because it was not guaranteed: the drained conclusion did not depend on the window it had been measured through. + +## Moved 2026-09-16 17:31:31 UTC-04:00 -- M2.16: the census that broke the prose around it + +### M2.16 -- Repair the garbled `Report` doc comment, and drop the two counts that had rotted beside it. *(completed 2026-09-16 17:31:31 UTC-04:00)* + +`src/report.rs` opened its `Report` sink doc with a dangling fragment -- a title line, a blank line, +then "is arithmetic. Every renderer writes through ..." -- and further down repeated the bare word +"signatures." after the sentence that already ended in it. Both were introduced on 2026-09-09. + +**The history is the point, and it was recovered rather than guessed.** `b5594860` wrote the passage +with a full sentence -- "This is the answer to 'how does a formatted line reach the sink', and the +reason it is a `std::fmt::Write` adapter rather than a method on `Report` is arithmetic" -- and a +count of **504** write sites. `3827dc32`, titled "Update comments in report.rs for better clarity", +revised that count to **332** and, in the same edit, deleted the two lines that carried the sentence. +So the edit that maintained the census is the edit that broke the prose, which is as direct an +argument for CONTRACT INTEGRITY rule 4 as this repository has produced. + +The two homes were fixed together, because fixing one alone would have created a fresh disagreement: + +- **`src/report.rs`** states the property instead of the count. `String` already implements + `fmt::Write`, so a sink that does too leaves every write site untouched and moves only the renderer + signatures. That is what makes the point, and it cannot rot. The lost sentence is restored, the + duplicated word removed, and the census delegated to the design note by link. +- **`DESIGN-NOTES.md`** keeps the numbers, because there they *are* the finding: the decision was + taken by counting, and the entry contrasts M1's estimate of "upwards of 160" with what re-measuring + found. They are now pinned as the census *as it stood on 2026-09-09 when the decision was taken* + rather than stated in the present tense as a description of the crate now, and the passage no longer + repeats the figure four times to make its argument. + +**A third home was found by sweeping and deliberately left alone.** `COMPLETED-CHECKLIST.md`'s +archived `M1.1` entry restates 332 and 18. It is pre-existing on main rather than authored by this +branch, it sits in an append-only archive dated by its own heading, and there the count is the whole +of the record -- the entry exists to say that the estimate was wrong and that re-measuring decided +the question. Rewriting it would have been an archive rewrite in service of tidiness. The design +note's new wording agrees with it rather than contradicting it. diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index c9af3e494..5f897f4fe 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1162,18 +1162,28 @@ The choice was between giving `Report` a method taking `fmt::Arguments` (with a `report_line!` macro), implementing `fmt::Write` on a sink so existing `writeln!` calls keep working, and keeping the `String` while flushing it at line boundaries. **It was decided by counting rather than by taste.** Every -renderer already writes through `writeln!(out, ...)` against a `String`'s -`fmt::Write`, at **332 sites** in this crate; only 18 functions take the `&mut -String` those sites write into. A sink method would have been the most explicit -option and would have rewritten all 332; `fmt::Write` moves the 18 and leaves -the 332 untouched, because `String` implements `fmt::Write` too and the call -sites cannot tell the difference. - -Worth recording that M1 estimated "upwards of 160" of those sites. The real -figure is twice that, and it is the whole of the argument -- an option whose -cost is "rewrite every call site" is affordable at 160 and is not at 332. A -plan's estimate is worth re-measuring at the moment it becomes a decision. - +renderer already wrote through `writeln!(out, ...)` against a `String`'s +`fmt::Write`, at far more sites than there were functions taking the `&mut +String` they wrote into. A sink method would have been the most explicit option +and would have rewritten every one of those write sites; `fmt::Write` moves the +signatures and leaves the write sites untouched, because `String` implements +`fmt::Write` too and the call sites cannot tell the difference. + +**The census as it stood when the decision was taken, on 2026-09-09: 332 write +sites against 18 signatures.** It is recorded as what was measured that day, not +as a description of the crate now, and the distinction earns its place here. The +same passage in the sink's own doc comment first carried 504 sites and "about +twenty" functions; the edit that revised those figures is the same edit that +dropped a sentence out of that comment and left it reading "is arithmetic" with +nothing before it. Keeping the census true by hand is what broke the prose +around it. The relation above is what the decision actually turned on, and it +cannot rot. + +Worth recording that M1 estimated "upwards of 160" of those sites, and the +measured figure was roughly twice that. The gap is the whole of the argument -- +an option whose cost is "rewrite every call site" is affordable at 160 and is +not at twice that. A plan's estimate is worth re-measuring at the moment it +becomes a decision. ### What the adapter has to reassemble, and why that is not a detail `fmt::Write` is **line-agnostic**: `write_str` receives whatever slices the diff --git a/crates/windows-platform-probes/src/report.rs b/crates/windows-platform-probes/src/report.rs index 781e09834..6fa4cbf91 100644 --- a/crates/windows-platform-probes/src/report.rs +++ b/crates/windows-platform-probes/src/report.rs @@ -92,13 +92,21 @@ impl Captured { /// A [`Report`] a renderer can `writeln!` into directly. /// -/// is arithmetic. Every renderer writes through `writeln!(out, ...)` against a -/// `String`, at **332 sites** across this crate; a sink method taking +/// This is the answer to "how does a formatted line reach the sink", and the +/// reason it is a [`std::fmt::Write`] adapter rather than a method on [`Report`] +/// is a property rather than a count. Every renderer writes through +/// `writeln!(out, ...)` against a `String`; a sink method taking /// `fmt::Arguments` would have been explicit but would have rewritten every one -/// of them, while `String` already implements `fmt::Write`, so a sink that does -/// too lets those sites stand untouched and moves only 18 renderer signatures. -/// signatures. The recorded reasoning is in -/// [DESIGN-NOTES.md](../DESIGN-NOTES.md#d-streaming-report). +/// of those sites, while `String` already implements `fmt::Write` -- so a sink +/// that does too lets every write site stand untouched and moves only the +/// renderer signatures. +/// +/// The census that decided it is recorded in +/// [DESIGN-NOTES.md](../DESIGN-NOTES.md#d-streaming-report) rather than repeated +/// here, because a count in prose is a copy somebody has to keep true. This +/// passage carried one, and the edit that revised it is the same edit that +/// dropped the sentence above and left this comment reading "is arithmetic" +/// with nothing before it. /// /// # Lines are reassembled here, because `fmt::Write` does not speak in them /// From 7042a11c47c1e107a5137ea36788023b271cb101 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 17:47:00 -0400 Subject: [PATCH 082/139] docs(probes): check every layout median, not just the largest Reported by review, and correct. `summarise.js` closed with inside the control band: which tests one median. A layout median *below* the control band's floor passes it unchanged and goes unreported -- so the artifact committed to substantiate "every layout median sits inside the control band" did not check that claim. The committed data clears the floor by 0.04x, so the published `true` was right by luck rather than by construction. The conclusion is unchanged: 18 medians spanning 0.96x to 1.12x against a control of 0.92x to 1.25x. What changed is that the script now establishes it -- it filters every median against both bounds, reports the medians' own range so a reader can check the containment by eye, and names any that fall outside. Sabotage-verified against a perturbed copy of the runs: driving three medians to 0.50x, below the floor while leaving the largest in band, flips the answer to `false` and lists them. The previous check reported `true` on that input. This is the fifth "test weaker than its name" on this branch and the fourth written by one of these rounds' own fixes -- this time in the artifact whose stated purpose is that the derivation "can be checked rather than taken on trust". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/summarise.js | 18 +++++++++++++++--- .../2026-09-16-drained-handshake/summary.txt | 4 ++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index df8f1efea..7f81b025d 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -87,8 +87,20 @@ console.log( ); console.log(` median: ${median(every).toFixed(2)}x`); console.log(""); -const widest = Math.max(...medians); -console.log(`widest layout median: ${widest.toFixed(2)}x`); +const low = Math.min(...every); +const high = Math.max(...every); +// **Every** layout median, not just the largest. The claim this capture is +// cited for is that all of them sit inside the control band, and an earlier +// version of this check tested `Math.max(...medians)` alone -- which passes +// unchanged while a median below the band's floor goes unreported. The +// committed data happens to clear the floor, so that check was right by luck +// rather than by construction. +const outside = medians.filter((m) => m < low || m > high); console.log( - `inside the control band: ${Math.min(...every) <= widest && widest <= Math.max(...every)}`, + `layout medians: ${medians.length}, spanning ` + + `${Math.min(...medians).toFixed(2)}x to ${Math.max(...medians).toFixed(2)}x`, ); +console.log(`every layout median inside the control band: ${outside.length === 0}`); +if (outside.length > 0) { + console.log(` outside: ${outside.map((m) => `${m.toFixed(2)}x`).join(", ")}`); +} diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt index 9195e911b..ecd883f89 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt @@ -14,5 +14,5 @@ same-code control (reserving_mpsc vs reserving 32/32), drained span: 0.92x to 1.25x median: 1.01x -widest layout median: 1.12x -inside the control band: true +layout medians: 18, spanning 0.96x to 1.12x +every layout median inside the control band: true From 681983594abfc59acf550c1bd3d6ef9e92386904 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 18:07:50 -0400 Subject: [PATCH 083/139] fix(probes): drain once before announcing, and stop a report citing a sweep it did not run Two findings from PR review, both against this branch's own recent work. **The handshake published readiness before the consumer had popped anything.** `M4.3`'s target said the flag is set *after entering* the drain loop; it was set before. The consumer could be descheduled between the store and its first `pop`, letting producers into the timed region against a queue nobody had taken from yet -- the same undrained opening in a narrower form, and the one the item exists to remove. Applied to all four drained timers. The prose is corrected with it, because it claimed more than the code did. `await_consumer` now states the guarantee exactly: no producer begins timing until the consumer has executed its pop path at least once. It does **not** guarantee continuous draining, and no flag could -- the consumer can be descheduled afterwards as it can at any point in the run. Saying the window was "closed" was the overstatement; what it is now is narrowed to a fact rather than an intention. **The report asserted a seven-run finding inside a single-run report.** The drained comparison's footer said the control "across seven runs was not zero, and was wide enough to swallow the layout rows" -- a figure from a separate sweep, printed beneath a table produced by one invocation, so a reader on a fresh host gets a claim their own data neither produced nor supports. It also drew the conclusion rather than leaving it, which rule 5 forbids in rendered prose. It now says what the row is and stops. The capture is retaken against this commit rather than carried forward, since the handshake change moves the drained numbers again. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention.rs | 4 +- .../src/queue_contention.rs | 62 ++++++++++++++----- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention.rs index b3bf31dc1..f02b347f3 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention.rs @@ -371,11 +371,11 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " what 'no difference' looks like on this host -- which across seven" + " what 'no difference' looks like on this host -- read it against" ); let _ = writeln!( out, - " runs was not zero, and was wide enough to swallow the layout rows." + " the layout rows before calling any of them apart." ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index f556c1ae0..5d55c4324 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -1088,7 +1088,7 @@ impl Drop for StopOnDrop { } } -/// Holds a producer until the consumer is actually draining. +/// Holds a producer until the consumer has drained at least once. /// /// **The gate is not enough, and the difference is the regime being measured.** /// Arriving at the gate proves the consumer exists, is scheduled and is past @@ -1099,6 +1099,13 @@ impl Drop for StopOnDrop { /// than by thread creation, which is why the gate was still worth having, but it /// was not zero. /// +/// **What this guarantees, stated exactly.** No producer begins timing until the +/// consumer has executed its pop path at least once. It does *not* guarantee the +/// consumer is draining continuously from then on -- nothing a flag can express +/// would, since the consumer can be descheduled at any point afterwards, as it +/// can at any point during the run. What it removes is the case where producers +/// push into a queue whose consumer has not yet run at all. +/// /// `Acquire`/`Release` rather than `Relaxed`, though the flag carries no data: /// this is the standing "promote the load" answer recorded in the queue crate's /// [D-40](../../windows-waitable-queues/DESIGN-NOTES.md#d-40) -- an acquire that @@ -1131,12 +1138,13 @@ fn time_drained_mpsc(producers: usize) -> Repetition { // The gate alone does not finish the job, which is why `await_consumer` // exists. Arriving proves the consumer exists, is scheduled and is past // start-up; it does not prove the consumer has reached its first `pop`, and - // the gate releases every party together. The handshake closes that - // remainder: the consumer announces that it is draining, and producers hold - // until they see it. This is the M4.3 change, and it MOVED the drained - // numbers -- the figures taken before it are kept beside the ones taken - // after rather than replaced, because they measure two different pieces of - // code. + // the gate releases every party together. The handshake narrows that + // remainder to a stated guarantee: no producer begins timing until the + // consumer has executed its pop path at least once. Continuous draining is + // not guaranteed and cannot be by a flag. This is the M4.3 change, and it + // MOVED the drained numbers -- the figures taken before it are kept beside + // the ones taken after rather than replaced, because they measure two + // different pieces of code. let gate = start_gate(producers + 1); let consumer_gate = Arc::clone(&gate); @@ -1144,8 +1152,14 @@ fn time_drained_mpsc(producers: usize) -> Repetition { if !consumer_gate.arrive_and_wait() { return rx.refused(); } - // Announced before the drain loop, so producers start against a - // consumer that is running rather than one merely spawned. + // **One drain attempt BEFORE announcing, not merely reaching the loop.** + // Publishing first proves only that the consumer is about to drain: it + // can be descheduled between the store and its first `pop`, which is the + // same undrained opening in a narrower form. Popping first makes the + // announcement mean `this consumer has executed the pop path`, which is a + // fact rather than an intention. The queue is empty here, so it costs one + // failed pop, and it happens before any producer has started timing. + let _ = rx.pop(); consumer_ready.store(true, Ordering::Release); // Spin rather than park: the doorbell's cost is `doorbell_cost`'s // question, and parking here would measure that instead of the claim. @@ -1246,8 +1260,14 @@ fn time_drained_reserving(producers: usize) -> Repetition { if !consumer_gate.arrive_and_wait() { return rx.refused(); } - // Announced before the drain loop, so producers start against a - // consumer that is running rather than one merely spawned. + // **One drain attempt BEFORE announcing, not merely reaching the loop.** + // Publishing first proves only that the consumer is about to drain: it + // can be descheduled between the store and its first `pop`, which is the + // same undrained opening in a narrower form. Popping first makes the + // announcement mean `this consumer has executed the pop path`, which is a + // fact rather than an intention. The queue is empty here, so it costs one + // failed pop, and it happens before any producer has started timing. + let _ = rx.pop(); consumer_ready.store(true, Ordering::Release); while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} @@ -1327,8 +1347,14 @@ fn time_drained_permit(producers: usize) -> Repetition { if !consumer_gate.arrive_and_wait() { return rx.refused(); } - // Announced before the drain loop, so producers start against a - // consumer that is running rather than one merely spawned. + // **One drain attempt BEFORE announcing, not merely reaching the loop.** + // Publishing first proves only that the consumer is about to drain: it + // can be descheduled between the store and its first `pop`, which is the + // same undrained opening in a narrower form. Popping first makes the + // announcement mean `this consumer has executed the pop path`, which is a + // fact rather than an intention. The queue is empty here, so it costs one + // failed pop, and it happens before any producer has started timing. + let _ = rx.pop(); consumer_ready.store(true, Ordering::Release); while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} @@ -1460,8 +1486,14 @@ fn time_drained_layout(producers: usize) -> Repetition if !consumer_gate.arrive_and_wait() { return rx.refused(); } - // Announced before the drain loop, so producers start against a - // consumer that is running rather than one merely spawned. + // **One drain attempt BEFORE announcing, not merely reaching the loop.** + // Publishing first proves only that the consumer is about to drain: it + // can be descheduled between the store and its first `pop`, which is the + // same undrained opening in a narrower form. Popping first makes the + // announcement mean `this consumer has executed the pop path`, which is a + // fact rather than an intention. The queue is empty here, so it costs one + // failed pop, and it happens before any producer has started timing. + let _ = rx.pop(); consumer_ready.store(true, Ordering::Release); while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} From 90b04c44250462bcd4387cb3779e425bd907467a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 18:12:41 -0400 Subject: [PATCH 084/139] docs(probes): retake the capture, and correct three stale statements The capture is retaken against `68198359`, because the handshake refinement in that commit moves the drained numbers again. The finding survives a second re-measurement: every layout median sits inside the same-code control band, which the summary script now actually checks. Both the band and the medians are wider in this capture than the last, which is the crate's existing finding about this host's control rather than a new one. Three corrections from the same review round, all on statements this branch itself made false: - The root design note said `windows-platform-probes` "commits no capture at all". True when written; falsified two commits later by this branch adding one. It now says what the gap was, that this is the first exception, and that the seven-run sweep the variance argument rests on still has none -- so the section is narrowed rather than quietly contradicted by the artifact sitting next to it. - `report.rs` described renderers as writing against a `String` in the present tense. They take `&mut dyn fmt::Write` and are handed a `LineSink`; the `String` is the shape the decision was taken *against*. The design note had already been put in the past tense for exactly this reason and the rustdoc had not -- the same fix landing at one of two sites, again. - `summarise.js` was missing the repository's copyright line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 13 +- .../2026-09-16-drained-handshake/README.md | 4 +- .../2026-09-16-drained-handshake/run1.txt | 230 +++++++++--------- .../2026-09-16-drained-handshake/run2.txt | 230 +++++++++--------- .../2026-09-16-drained-handshake/run3.txt | 230 +++++++++--------- .../2026-09-16-drained-handshake/summarise.js | 1 + .../2026-09-16-drained-handshake/summary.txt | 18 +- crates/windows-platform-probes/src/report.rs | 7 +- 8 files changed, 370 insertions(+), 363 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 7991d1016..4099e7d44 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1980,11 +1980,16 @@ proportion that restated two counts **given four words earlier in the same sente them wrong -- it said "in both cases roughly 60%" where one of the two cases was 57 of 61. The data was adjacent and the summary of it was false, because prose is not checkable and nobody checks it. -**This repository already contains the better pattern and did not apply it here.** +**This repository already contains the better pattern, and this branch was the first to apply it +in the probe crate.** [`mutation-sweeps/2026-09-02/`](mutation-sweeps/2026-09-02) is a dated, committed capture directory: data as an artifact, cited -rather than retyped. `windows-platform-probes`, which produces the most-cited numbers in the -workspace, commits no capture at all -- every figure it has ever published reached its document by -hand. +rather than retyped. `windows-platform-probes` produces the most-cited numbers in the workspace and +committed no capture at all when this section was written -- every figure it had published reached +its document by hand. The re-measurement that `M4.3` forced is the first exception: +[`crates/windows-platform-probes/captures/2026-09-16-drained-handshake/`](crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md) +commits the raw runs, the script that derives the summary, and its output. The seven-run sweep that +the variance argument rests on still has no committed capture, so the gap this section describes is +narrowed rather than closed. So the principle, which holds regardless of which mechanism is eventually chosen: diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md index 44acde42f..0e3a51d35 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md @@ -19,8 +19,8 @@ each is labelled with the instrument that produced it. | Profile | release | | Sampling | 50,000 pushes per producer, median of 5 repetitions, one untimed warmup pass | | Runs | 3 whole-probe invocations, in [run1.txt](run1.txt), [run2.txt](run2.txt), [run3.txt](run3.txt) | -| Instrument | `probe-queue-contention`, built from `04e6d825` (the commit that added the handshake) | -| Taken | 2026-09-16 UTC-04:00 | +| Instrument | `probe-queue-contention`, built from `68198359` (the commit that made the consumer drain once before announcing readiness) | +| Taken | 2026-09-16 18:12 UTC-04:00 | The host is the same machine as the capture the crate README carries, so the two are comparable; nothing here says anything about any other hardware, and the diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run1.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run1.txt index 2942ebb43..e1e2f63e4 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run1.txt +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run1.txt @@ -8,99 +8,99 @@ sampling: 50000 pushes per producer, median of 5 repetitions, one untimed warmup -- isolated: producers only, capacity large enough that nothing is refused -- shape producers ns/op ops/sec refusals ns/op range spread -baseline_fetch_add 1 2.7 373692078 0 2.3-3.2 1.40x -slotwise_mpsc 1 5.9 168350168 0 5.8-6.6 1.14x -reserving_mpsc 1 5.6 180050414 0 5.4-6.2 1.15x -permit_mpsc 1 7.9 126678490 0 7.9-8.1 1.03x -reserving(32/32) 1 5.4 184094256 0 5.4-5.6 1.04x -reserving(16/48) 1 5.4 185185185 0 5.4-8.5 1.58x -reserving(8/56) 1 5.7 176740898 0 5.4-5.9 1.09x -reserving(64/64) 1 7.4 134372481 0 7.4-7.5 1.00x -baseline_fetch_add 2 10.4 96061479 0 8.1-11.9 1.47x -slotwise_mpsc 2 47.6 21022536 0 30.1-56.3 1.87x -reserving_mpsc 2 34.6 28920959 0 18.4-37.4 2.04x -permit_mpsc 2 42.6 23472525 0 18.8-45.3 2.41x -reserving(32/32) 2 31.3 31933578 0 23.5-34.0 1.45x -reserving(16/48) 2 34.9 28687819 0 29.6-70.3 2.37x -reserving(8/56) 2 28.9 34622442 0 24.0-34.6 1.44x -reserving(64/64) 2 38.0 26322024 0 30.5-41.6 1.37x -baseline_fetch_add 4 14.3 70143443 0 13.0-14.6 1.13x -slotwise_mpsc 4 89.1 11224856 0 87.1-93.9 1.08x -reserving_mpsc 4 39.3 25432350 0 33.6-41.5 1.24x -permit_mpsc 4 30.4 32896360 0 30.0-31.8 1.06x -reserving(32/32) 4 33.1 30235687 0 32.5-35.7 1.10x -reserving(16/48) 4 34.5 28970812 0 31.2-38.1 1.22x -reserving(8/56) 4 36.9 27117541 0 35.5-37.6 1.06x -reserving(64/64) 4 51.9 19270236 0 46.3-68.9 1.49x -baseline_fetch_add 8 15.1 66408779 0 14.2-15.3 1.08x -slotwise_mpsc 8 134.2 7454064 0 129.4-142.6 1.10x -reserving_mpsc 8 38.0 26332074 0 36.4-47.8 1.31x -permit_mpsc 8 24.7 40512073 0 22.2-26.8 1.21x -reserving(32/32) 8 38.8 25754610 0 38.4-42.5 1.11x -reserving(16/48) 8 44.6 22409717 0 40.6-49.1 1.21x -reserving(8/56) 8 45.9 21773684 0 43.0-56.2 1.31x -reserving(64/64) 8 76.0 13152703 0 72.8-97.8 1.34x -baseline_fetch_add 16 14.5 68827862 0 14.4-14.7 1.02x -slotwise_mpsc 16 217.5 4598616 0 154.9-225.3 1.45x -reserving_mpsc 16 47.7 20975684 0 44.2-52.6 1.19x -permit_mpsc 16 23.4 42767255 0 20.9-24.1 1.15x -reserving(32/32) 16 45.1 22166252 0 43.2-49.0 1.13x -reserving(16/48) 16 62.5 16000704 0 55.0-67.6 1.23x -reserving(8/56) 16 55.0 18174672 0 48.1-62.5 1.30x -reserving(64/64) 16 154.4 6476139 0 142.2-184.2 1.30x -baseline_fetch_add 32 15.1 66355074 0 14.9-15.1 1.02x -slotwise_mpsc 32 225.0 4444205 0 206.1-243.2 1.18x -reserving_mpsc 32 49.1 20363540 0 48.6-53.8 1.11x -permit_mpsc 32 22.0 45390972 0 20.7-34.0 1.65x -reserving(32/32) 32 48.0 20817124 0 45.3-50.9 1.12x -reserving(16/48) 32 62.5 16006979 0 59.9-67.0 1.12x -reserving(8/56) 32 65.1 15367209 0 60.2-68.5 1.14x -reserving(64/64) 32 214.0 4673929 0 182.1-219.4 1.20x +baseline_fetch_add 1 2.3 432900433 0 2.2-2.3 1.04x +slotwise_mpsc 1 5.9 169204738 0 5.9-6.4 1.08x +reserving_mpsc 1 5.5 182415177 0 5.4-5.9 1.10x +permit_mpsc 1 7.9 127097102 0 7.8-7.9 1.01x +reserving(32/32) 1 5.7 175870559 0 5.4-5.9 1.09x +reserving(16/48) 1 5.4 186289121 0 5.3-5.7 1.06x +reserving(8/56) 1 5.4 185735513 0 5.4-5.8 1.07x +reserving(64/64) 1 7.5 134120172 0 7.4-8.0 1.08x +baseline_fetch_add 2 11.8 84774500 0 10.7-12.8 1.19x +slotwise_mpsc 2 57.5 17395237 0 56.9-63.7 1.12x +reserving_mpsc 2 34.1 29330674 0 32.6-37.9 1.16x +permit_mpsc 2 39.7 25160398 0 17.9-42.5 2.38x +reserving(32/32) 2 35.0 28573878 0 30.2-37.2 1.23x +reserving(16/48) 2 33.7 29638411 0 33.2-36.7 1.11x +reserving(8/56) 2 33.2 30110506 0 32.0-38.0 1.19x +reserving(64/64) 2 40.5 24717601 0 37.6-41.0 1.09x +baseline_fetch_add 4 14.9 67098333 0 14.1-15.9 1.12x +slotwise_mpsc 4 87.3 11450163 0 85.0-89.9 1.06x +reserving_mpsc 4 37.8 26456776 0 34.9-39.2 1.12x +permit_mpsc 4 30.7 32614722 0 30.1-89.4 2.97x +reserving(32/32) 4 36.3 27581261 0 35.4-39.6 1.12x +reserving(16/48) 4 37.1 26989825 0 34.0-40.0 1.18x +reserving(8/56) 4 35.0 28553481 0 30.6-40.9 1.34x +reserving(64/64) 4 61.8 16178743 0 49.2-64.3 1.31x +baseline_fetch_add 8 14.9 66969144 0 14.6-15.3 1.05x +slotwise_mpsc 8 132.4 7550033 0 129.2-147.7 1.14x +reserving_mpsc 8 38.4 26065764 0 36.0-49.9 1.39x +permit_mpsc 8 24.4 40931603 0 22.1-27.1 1.23x +reserving(32/32) 8 38.8 25798296 0 34.6-40.4 1.17x +reserving(16/48) 8 37.0 27019359 0 33.9-45.9 1.35x +reserving(8/56) 8 35.6 28093439 0 34.8-41.2 1.18x +reserving(64/64) 8 61.7 16203319 0 54.1-88.1 1.63x +baseline_fetch_add 16 14.7 67896153 0 14.6-14.8 1.02x +slotwise_mpsc 16 250.7 3988420 0 193.4-270.0 1.40x +reserving_mpsc 16 45.8 21854759 0 44.1-52.6 1.19x +permit_mpsc 16 22.4 44640117 0 21.1-23.8 1.13x +reserving(32/32) 16 52.9 18894528 0 50.7-55.8 1.10x +reserving(16/48) 16 62.2 16083150 0 59.8-69.2 1.16x +reserving(8/56) 16 68.0 14702963 0 66.4-74.4 1.12x +reserving(64/64) 16 216.7 4615734 0 206.7-218.5 1.06x +baseline_fetch_add 32 14.8 67525079 0 14.5-15.3 1.05x +slotwise_mpsc 32 227.3 4400348 0 223.9-268.5 1.20x +reserving_mpsc 32 53.0 18873756 0 51.0-56.1 1.10x +permit_mpsc 32 19.9 50292798 0 19.8-20.0 1.01x +reserving(32/32) 32 49.4 20238050 0 46.9-52.0 1.11x +reserving(16/48) 32 66.0 15141493 0 64.5-75.7 1.17x +reserving(8/56) 32 70.6 14172549 0 62.7-72.4 1.15x +reserving(64/64) 32 246.7 4054253 0 191.4-252.7 1.32x -- drained: a consumer popping continuously, capacity 1024 -- shape producers ns/op ops/sec refusals ns/op range spread -slotwise_mpsc 1 10.6 94215187 35 9.9-14.1 1.42x -reserving_mpsc 1 25.4 39373179 461 24.9-26.7 1.07x -permit_mpsc 1 61.2 16327064 2663 60.2-63.9 1.06x -reserving(32/32) 1 25.5 39227993 1041 20.5-25.7 1.25x -reserving(16/48) 1 27.4 36531015 2158 26.3-28.3 1.07x -reserving(8/56) 1 27.1 36859565 2543 26.4-28.3 1.07x -reserving(64/64) 1 27.4 36555052 205 25.6-29.1 1.14x -slotwise_mpsc 2 57.7 17343045 2045 36.9-74.8 2.02x -reserving_mpsc 2 61.8 16186206 166 55.7-70.5 1.27x -permit_mpsc 2 53.5 18704175 2913 51.8-58.4 1.13x -reserving(32/32) 2 65.9 15166222 2329 63.3-69.8 1.10x -reserving(16/48) 2 64.2 15565171 599 57.0-69.6 1.22x -reserving(8/56) 2 67.9 14737307 1300 67.5-70.7 1.05x -reserving(64/64) 2 70.4 14202125 113 66.4-72.6 1.09x -slotwise_mpsc 4 99.4 10058692 2496 95.9-112.1 1.17x -reserving_mpsc 4 90.9 11001282 0 86.3-101.2 1.17x -permit_mpsc 4 82.4 12137174 88978 59.7-84.6 1.42x -reserving(32/32) 4 83.7 11949216 145 81.8-93.1 1.14x -reserving(16/48) 4 95.6 10461017 0 93.7-100.0 1.07x -reserving(8/56) 4 95.4 10482785 0 95.0-99.6 1.05x -reserving(64/64) 4 106.2 9416462 88 100.2-113.8 1.14x -slotwise_mpsc 8 150.0 6664834 1058 145.8-159.1 1.09x -reserving_mpsc 8 154.2 6485168 0 145.0-161.2 1.11x -permit_mpsc 8 135.6 7374645 532659 121.4-147.0 1.21x -reserving(32/32) 8 156.4 6394721 212 143.7-163.6 1.14x -reserving(16/48) 8 160.3 6240220 75970 150.4-164.9 1.10x -reserving(8/56) 8 153.1 6533471 11751 141.3-161.8 1.15x -reserving(64/64) 8 166.4 6011268 1395 149.8-175.9 1.17x -slotwise_mpsc 16 340.3 2938673 2850654 306.4-526.9 1.72x -reserving_mpsc 16 330.7 3023617 3732261 261.5-414.1 1.58x -permit_mpsc 16 245.4 4074334 2735377 177.2-317.7 1.79x -reserving(32/32) 16 264.0 3788550 1493570 221.9-332.3 1.50x -reserving(16/48) 16 291.5 3430305 3649382 274.0-427.6 1.56x -reserving(8/56) 16 266.1 3757414 1448525 251.7-315.0 1.25x -reserving(64/64) 16 345.7 2893058 3598613 297.3-403.2 1.36x -slotwise_mpsc 32 601.6 1662156 19050487 495.9-707.5 1.43x -reserving_mpsc 32 741.1 1349358 29947662 546.0-1040.8 1.91x -permit_mpsc 32 513.0 1949410 12629644 439.9-557.0 1.27x -reserving(32/32) 32 705.4 1417609 28232093 603.7-812.8 1.35x -reserving(16/48) 32 588.6 1698996 17387314 518.8-724.3 1.40x -reserving(8/56) 32 560.8 1783052 16742286 541.7-677.7 1.25x -reserving(64/64) 32 716.7 1395263 22491959 504.0-755.6 1.50x +slotwise_mpsc 1 10.5 95274390 30 9.6-11.0 1.15x +reserving_mpsc 1 24.5 40889761 706 23.6-26.6 1.13x +permit_mpsc 1 60.5 16541503 437 59.7-70.6 1.18x +reserving(32/32) 1 25.0 40048058 707 21.3-26.7 1.26x +reserving(16/48) 1 25.3 39522567 1549 25.2-27.4 1.09x +reserving(8/56) 1 25.6 39047247 1231 24.6-27.9 1.13x +reserving(64/64) 1 29.3 34176350 2067 28.8-31.3 1.08x +slotwise_mpsc 2 74.3 13450804 360 68.3-81.1 1.19x +reserving_mpsc 2 66.5 15041892 2633 60.1-70.9 1.18x +permit_mpsc 2 55.6 17980114 10 54.4-56.3 1.03x +reserving(32/32) 2 67.0 14915355 92 65.0-67.4 1.04x +reserving(16/48) 2 66.2 15097302 1183 64.9-71.8 1.11x +reserving(8/56) 2 69.2 14454418 281 64.7-70.0 1.08x +reserving(64/64) 2 71.1 14056394 1050 65.6-73.8 1.13x +slotwise_mpsc 4 108.0 9255746 5979 97.8-112.9 1.16x +reserving_mpsc 4 92.8 10773598 0 91.4-100.6 1.10x +permit_mpsc 4 82.9 12055818 87923 71.3-84.5 1.19x +reserving(32/32) 4 94.5 10584867 808 90.2-100.9 1.12x +reserving(16/48) 4 100.0 9997001 0 98.5-105.6 1.07x +reserving(8/56) 4 94.3 10606141 1151 93.3-97.0 1.04x +reserving(64/64) 4 103.3 9683167 4388 92.7-112.6 1.21x +slotwise_mpsc 8 157.6 6343376 575 153.3-159.2 1.04x +reserving_mpsc 8 164.5 6078270 1755 147.9-172.7 1.17x +permit_mpsc 8 147.7 6772246 608108 136.6-159.7 1.17x +reserving(32/32) 8 165.0 6061488 4788 158.9-173.9 1.09x +reserving(16/48) 8 167.0 5989432 12075 163.2-168.3 1.03x +reserving(8/56) 8 165.9 6026556 6065 160.1-169.3 1.06x +reserving(64/64) 8 176.0 5681859 8970 166.3-185.4 1.11x +slotwise_mpsc 16 248.1 4030921 996020 244.6-333.4 1.36x +reserving_mpsc 16 275.5 3629124 1273380 258.4-346.1 1.34x +permit_mpsc 16 278.2 3594693 3179439 244.9-311.0 1.27x +reserving(32/32) 16 265.9 3760339 1045393 245.3-295.9 1.21x +reserving(16/48) 16 283.7 3524417 956012 257.9-319.5 1.24x +reserving(8/56) 16 267.3 3740662 1219744 249.3-347.0 1.39x +reserving(64/64) 16 255.7 3910307 1510719 250.3-328.6 1.31x +slotwise_mpsc 32 566.2 1766305 16022148 486.2-717.5 1.48x +reserving_mpsc 32 619.1 1615374 23168414 494.6-784.9 1.59x +permit_mpsc 32 483.8 2066835 11753534 416.8-572.5 1.37x +reserving(32/32) 32 654.8 1527084 25476853 466.5-765.1 1.64x +reserving(16/48) 32 806.1 1240549 29246593 574.9-905.9 1.58x +reserving(8/56) 32 586.1 1706172 17994934 497.6-800.6 1.61x +reserving(64/64) 32 445.5 2244654 9291698 429.6-826.5 1.92x interpretation: @@ -108,11 +108,11 @@ interpretation: producers slotwise reserving permit atomic floor 1 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] - 2 0.12x [0.10-0.22] 0.16x [0.14-0.34] 0.19x [0.17-0.43] 0.26x [0.19-0.40] - 4 0.07x [0.06-0.08] 0.14x [0.13-0.19] 0.26x [0.25-0.27] 0.19x [0.16-0.25] - 8 0.04x [0.04-0.05] 0.15x [0.11-0.17] 0.32x [0.29-0.37] 0.18x [0.15-0.23] - 16 0.03x [0.03-0.04] 0.12x [0.10-0.14] 0.34x [0.33-0.39] 0.18x [0.16-0.22] - 32 0.03x [0.02-0.03] 0.11x [0.10-0.13] 0.36x [0.23-0.39] 0.18x [0.15-0.22] + 2 0.10x [0.09-0.11] 0.16x [0.14-0.18] 0.20x [0.18-0.44] 0.20x [0.17-0.22] + 4 0.07x [0.07-0.07] 0.15x [0.14-0.17] 0.26x [0.09-0.26] 0.15x [0.14-0.16] + 8 0.04x [0.04-0.05] 0.14x [0.11-0.16] 0.32x [0.29-0.36] 0.15x [0.14-0.16] + 16 0.02x [0.02-0.03] 0.12x [0.10-0.13] 0.35x [0.33-0.37] 0.16x [0.15-0.16] + 32 0.03x [0.02-0.03] 0.10x [0.10-0.12] 0.40x [0.39-0.40] 0.16x [0.15-0.16] Read as: throughput at N producers divided by throughput at one. 1.00 means N threads together push no faster than one did. @@ -130,12 +130,12 @@ interpretation: producers slotwise reserving reserving/slotwise permit permit/reserving ns/op ns/op ratio [bound] ns/op ratio [bound] - 1 10.6 25.4 2.39x [1.77-2.70] 61.2 2.41x [2.26-2.57] - 2 57.7 61.8 1.07x [0.74-1.91] 53.5 0.87x [0.73-1.05] - 4 99.4 90.9 0.91x [0.77-1.06] 82.4 0.91x [0.59-0.98] - 8 150.0 154.2 1.03x [0.91-1.11] 135.6 0.88x [0.75-1.01] - 16 340.3 330.7 0.97x [0.50-1.35] 245.4 0.74x [0.43-1.22] - 32 601.6 741.1 1.23x [0.77-2.10] 513.0 0.69x [0.42-1.02] + 1 10.5 24.5 2.33x [2.14-2.77] 60.5 2.47x [2.24-2.99] + 2 74.3 66.5 0.89x [0.74-1.04] 55.6 0.84x [0.77-0.94] + 4 108.0 92.8 0.86x [0.81-1.03] 82.9 0.89x [0.71-0.93] + 8 157.6 164.5 1.04x [0.93-1.13] 147.7 0.90x [0.79-1.08] + 16 248.1 275.5 1.11x [0.77-1.41] 278.2 1.01x [0.71-1.20] + 32 566.2 619.1 1.09x [0.69-1.61] 483.8 0.78x [0.53-1.16] `reserving_mpsc` reads the consumer's position on every push and `slotwise_mpsc` does not. This regime is where that read is at its @@ -165,8 +165,8 @@ interpretation: difference between them is not thereby noise. Read it against a control before calling it either way: the reserving_mpsc row and the 32/32 row above are the same code, so the gap between them is - what 'no difference' looks like on this host -- which across seven - runs was not zero, and was wide enough to swallow the layout rows. + what 'no difference' looks like on this host -- read it against + the layout rows before calling any of them apart. 64/64 vs 32/32 is the double-width layout's effect on the whole push path -- what moving the recurrence to 2^64 costs, against 8/56 moving it to 2^56. Both defer the recurrence rather than @@ -174,21 +174,21 @@ interpretation: -- isolated -- producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs - 1 5.4 5.4 5.7 7.4 0.99x [0.95-1.56] 1.04x [0.96-1.09] 1.37x [1.32-1.38] - 2 31.3 34.9 28.9 38.0 1.11x [0.87-3.00] 0.92x [0.71-1.47] 1.21x [0.90-1.77] - 4 33.1 34.5 36.9 51.9 1.04x [0.87-1.17] 1.11x [0.99-1.16] 1.57x [1.29-2.12] - 8 38.8 44.6 45.9 76.0 1.15x [0.95-1.28] 1.18x [1.01-1.47] 1.96x [1.71-2.55] - 16 45.1 62.5 55.0 154.4 1.39x [1.12-1.56] 1.22x [0.98-1.45] 3.42x [2.90-4.26] - 32 48.0 62.5 65.1 214.0 1.30x [1.18-1.48] 1.35x [1.18-1.51] 4.45x [3.58-4.84] + 1 5.7 5.4 5.4 7.5 0.94x [0.91-1.05] 0.95x [0.91-1.07] 1.31x [1.26-1.49] + 2 35.0 33.7 33.2 40.5 0.96x [0.89-1.21] 0.95x [0.86-1.26] 1.16x [1.01-1.36] + 4 36.3 37.1 35.0 61.8 1.02x [0.86-1.13] 0.97x [0.77-1.16] 1.70x [1.24-1.82] + 8 38.8 37.0 35.6 61.7 0.95x [0.84-1.32] 0.92x [0.86-1.19] 1.59x [1.34-2.54] + 16 52.9 62.2 68.0 216.7 1.17x [1.07-1.37] 1.29x [1.19-1.47] 4.09x [3.71-4.31] + 32 49.4 66.0 70.6 246.7 1.34x [1.24-1.61] 1.43x [1.21-1.54] 4.99x [3.68-5.39] -- drained -- producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs - 1 25.5 27.4 27.1 27.4 1.07x [1.02-1.38] 1.06x [1.02-1.38] 1.07x [0.99-1.42] - 2 65.9 64.2 67.9 70.4 0.97x [0.82-1.10] 1.03x [0.97-1.12] 1.07x [0.95-1.15] - 4 83.7 95.6 95.4 106.2 1.14x [1.01-1.22] 1.14x [1.02-1.22] 1.27x [1.08-1.39] - 8 156.4 160.3 153.1 166.4 1.02x [0.92-1.15] 0.98x [0.86-1.13] 1.06x [0.92-1.22] - 16 264.0 291.5 266.1 345.7 1.10x [0.82-1.93] 1.01x [0.76-1.42] 1.31x [0.89-1.82] - 32 705.4 588.6 560.8 716.7 0.83x [0.64-1.20] 0.80x [0.67-1.12] 1.02x [0.62-1.25] + 1 25.0 25.3 25.6 29.3 1.01x [0.94-1.29] 1.03x [0.92-1.31] 1.17x [1.08-1.47] + 2 67.0 66.2 69.2 71.1 0.99x [0.96-1.10] 1.03x [0.96-1.08] 1.06x [0.97-1.14] + 4 94.5 100.0 94.3 103.3 1.06x [0.98-1.17] 1.00x [0.93-1.07] 1.09x [0.92-1.25] + 8 165.0 167.0 165.9 176.0 1.01x [0.94-1.06] 1.01x [0.92-1.07] 1.07x [0.96-1.17] + 16 265.9 283.7 267.3 255.7 1.07x [0.87-1.30] 1.01x [0.84-1.41] 0.96x [0.85-1.34] + 32 654.8 806.1 586.1 445.5 1.23x [0.75-1.94] 0.90x [0.65-1.72] 0.68x [0.56-1.77] the 32/32 row and the reserving_mpsc row above are the same configuration run twice, so the gap between them is this host's diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run2.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run2.txt index d5819f18d..d9e203762 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run2.txt +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run2.txt @@ -8,99 +8,99 @@ sampling: 50000 pushes per producer, median of 5 repetitions, one untimed warmup -- isolated: producers only, capacity large enough that nothing is refused -- shape producers ns/op ops/sec refusals ns/op range spread -baseline_fetch_add 1 2.3 432525952 0 1.9-2.5 1.26x -slotwise_mpsc 1 6.0 167954316 0 5.9-6.0 1.02x -reserving_mpsc 1 5.4 184638109 0 5.4-5.7 1.05x -permit_mpsc 1 8.2 121536218 0 8.0-9.4 1.18x -reserving(32/32) 1 5.4 184638109 0 5.4-5.8 1.07x -reserving(16/48) 1 5.5 181686047 0 5.4-5.6 1.04x -reserving(8/56) 1 5.6 179533214 0 5.4-6.0 1.11x -reserving(64/64) 1 7.4 134408602 0 7.4-7.7 1.04x -baseline_fetch_add 2 11.2 89182199 0 6.6-13.5 2.06x -slotwise_mpsc 2 59.4 16830767 0 49.1-68.5 1.40x -reserving_mpsc 2 35.3 28317381 0 33.9-37.0 1.09x -permit_mpsc 2 40.9 24465430 0 26.8-44.2 1.65x -reserving(32/32) 2 32.5 30809995 0 24.5-34.6 1.41x -reserving(16/48) 2 34.9 28624589 0 25.2-35.9 1.42x -reserving(8/56) 2 33.2 30080616 0 31.3-35.1 1.12x -reserving(64/64) 2 38.6 25883936 0 34.6-39.2 1.13x -baseline_fetch_add 4 15.6 64094347 0 14.4-16.3 1.13x -slotwise_mpsc 4 86.3 11588493 0 75.5-88.8 1.18x -reserving_mpsc 4 35.0 28567348 0 29.3-40.5 1.38x -permit_mpsc 4 31.6 31670124 0 29.4-32.1 1.09x -reserving(32/32) 4 38.9 25697692 0 36.0-41.0 1.14x -reserving(16/48) 4 37.9 26380004 0 36.4-38.7 1.06x -reserving(8/56) 4 35.5 28167824 0 34.0-40.8 1.20x -reserving(64/64) 4 47.2 21202837 0 41.4-54.4 1.31x -baseline_fetch_add 8 15.3 65553352 0 14.1-16.9 1.20x -slotwise_mpsc 8 133.8 7475266 0 123.1-140.3 1.14x -reserving_mpsc 8 39.6 25279656 0 37.8-47.4 1.26x -permit_mpsc 8 25.5 39251860 0 23.7-27.1 1.14x -reserving(32/32) 8 42.4 23611496 0 36.4-43.8 1.20x -reserving(16/48) 8 53.3 18778285 0 45.4-55.9 1.23x -reserving(8/56) 8 37.6 26602289 0 33.4-47.2 1.41x -reserving(64/64) 8 71.6 13960777 0 49.6-98.2 1.98x -baseline_fetch_add 16 14.9 67111279 0 14.5-15.2 1.05x -slotwise_mpsc 16 198.5 5037790 0 188.8-214.9 1.14x -reserving_mpsc 16 49.7 20128673 0 49.4-54.0 1.09x -permit_mpsc 16 20.7 48241012 0 20.1-23.6 1.18x -reserving(32/32) 16 47.6 20992747 0 44.0-47.7 1.09x -reserving(16/48) 16 61.0 16405814 0 57.7-64.9 1.13x -reserving(8/56) 16 61.4 16299586 0 53.0-65.9 1.24x -reserving(64/64) 16 166.7 5998036 0 124.4-192.9 1.55x -baseline_fetch_add 32 15.3 65501290 0 14.9-15.6 1.04x -slotwise_mpsc 32 225.5 4435428 0 218.8-230.0 1.05x -reserving_mpsc 32 51.5 19404288 0 48.4-53.4 1.10x -permit_mpsc 32 21.4 46727607 0 21.2-22.1 1.04x -reserving(32/32) 32 50.0 20014611 0 46.1-50.1 1.09x -reserving(16/48) 32 62.9 15899104 0 51.8-67.3 1.30x -reserving(8/56) 32 53.2 18804150 0 51.1-60.3 1.18x -reserving(64/64) 32 201.4 4964755 0 168.8-209.4 1.24x +baseline_fetch_add 1 2.3 432152118 0 2.3-2.6 1.14x +slotwise_mpsc 1 6.0 166722241 0 5.9-6.3 1.07x +reserving_mpsc 1 5.4 184774575 0 5.4-5.9 1.09x +permit_mpsc 1 8.1 123609394 0 8.1-8.7 1.08x +reserving(32/32) 1 6.8 147405660 0 5.4-9.1 1.67x +reserving(16/48) 1 6.4 157331655 0 6.2-6.8 1.11x +reserving(8/56) 1 6.6 152207002 0 6.0-6.8 1.14x +reserving(64/64) 1 11.8 85048478 0 10.1-12.0 1.19x +baseline_fetch_add 2 12.5 80179602 0 11.8-12.8 1.08x +slotwise_mpsc 2 58.8 17014326 0 44.4-61.3 1.38x +reserving_mpsc 2 35.9 27845845 0 29.5-37.8 1.28x +permit_mpsc 2 43.1 23208856 0 40.4-43.9 1.09x +reserving(32/32) 2 36.0 27808676 0 32.6-37.8 1.16x +reserving(16/48) 2 33.6 29717682 0 31.1-35.3 1.13x +reserving(8/56) 2 32.8 30522235 0 24.4-36.5 1.50x +reserving(64/64) 2 39.5 25342119 0 35.8-40.0 1.12x +baseline_fetch_add 4 13.8 72513687 0 13.4-15.8 1.18x +slotwise_mpsc 4 92.3 10837406 0 90.4-93.0 1.03x +reserving_mpsc 4 36.8 27173175 0 35.5-39.2 1.11x +permit_mpsc 4 32.0 31285685 0 31.3-32.5 1.04x +reserving(32/32) 4 38.0 26332420 0 34.6-40.1 1.16x +reserving(16/48) 4 32.7 30609122 0 30.5-35.8 1.17x +reserving(8/56) 4 35.3 28292945 0 30.6-41.1 1.35x +reserving(64/64) 4 52.1 19205654 0 42.5-57.7 1.36x +baseline_fetch_add 8 15.8 63477957 0 14.3-17.2 1.21x +slotwise_mpsc 8 141.9 7044933 0 136.3-148.1 1.09x +reserving_mpsc 8 38.5 25992254 0 35.8-40.3 1.13x +permit_mpsc 8 25.7 38934745 0 25.7-26.9 1.05x +reserving(32/32) 8 38.2 26204077 0 36.2-44.6 1.23x +reserving(16/48) 8 36.4 27492543 0 36.1-38.4 1.06x +reserving(8/56) 8 43.6 22933018 0 34.7-45.3 1.31x +reserving(64/64) 8 78.3 12770006 0 62.6-103.8 1.66x +baseline_fetch_add 16 14.9 66962417 0 14.5-15.1 1.04x +slotwise_mpsc 16 248.2 4029196 0 196.7-268.3 1.36x +reserving_mpsc 16 50.4 19839203 0 50.0-54.6 1.09x +permit_mpsc 16 20.2 49499437 0 19.7-22.9 1.16x +reserving(32/32) 16 46.2 21639635 0 44.7-46.9 1.05x +reserving(16/48) 16 57.9 17263293 0 55.6-60.7 1.09x +reserving(8/56) 16 52.7 18991098 0 49.5-57.7 1.17x +reserving(64/64) 16 184.6 5417144 0 132.3-188.8 1.43x +baseline_fetch_add 32 15.1 66207901 0 14.9-15.4 1.03x +slotwise_mpsc 32 227.5 4395744 0 217.2-229.7 1.06x +reserving_mpsc 32 53.4 18722143 0 48.7-55.5 1.14x +permit_mpsc 32 22.1 45231856 0 21.8-22.5 1.03x +reserving(32/32) 32 52.3 19129466 0 50.7-54.7 1.08x +reserving(16/48) 32 74.5 13425229 0 67.2-76.2 1.13x +reserving(8/56) 32 66.0 15140432 0 64.0-71.6 1.12x +reserving(64/64) 32 185.3 5395613 0 173.8-231.3 1.33x -- drained: a consumer popping continuously, capacity 1024 -- shape producers ns/op ops/sec refusals ns/op range spread -slotwise_mpsc 1 9.8 101729400 9 9.1-11.1 1.22x -reserving_mpsc 1 25.2 39610235 1026 23.8-27.8 1.17x -permit_mpsc 1 61.0 16394518 543 59.6-62.9 1.05x -reserving(32/32) 1 26.3 37962190 888 25.5-28.3 1.11x -reserving(16/48) 1 28.0 35670971 315 26.7-29.9 1.12x -reserving(8/56) 1 27.9 35888602 389 25.3-30.6 1.21x -reserving(64/64) 1 29.8 33518804 1321 26.6-31.2 1.17x -slotwise_mpsc 2 77.5 12906723 372 20.6-80.0 3.88x -reserving_mpsc 2 69.2 14457344 204 62.3-69.9 1.12x -permit_mpsc 2 54.3 18414511 2359 52.5-55.3 1.05x -reserving(32/32) 2 67.6 14798810 426 63.8-70.2 1.10x -reserving(16/48) 2 70.0 14295721 0 65.3-72.2 1.11x -reserving(8/56) 2 67.2 14890481 4459 65.3-71.5 1.10x -reserving(64/64) 2 72.0 13888696 1013 69.5-73.8 1.06x -slotwise_mpsc 4 104.9 9529573 32140 90.0-114.0 1.27x -reserving_mpsc 4 94.3 10607266 6854 87.9-103.6 1.18x -permit_mpsc 4 80.3 12450432 85539 51.0-83.3 1.63x -reserving(32/32) 4 94.0 10641298 4458 84.7-102.0 1.20x -reserving(16/48) 4 95.6 10458063 0 91.5-101.4 1.11x -reserving(8/56) 4 99.4 10061678 564 92.8-102.2 1.10x -reserving(64/64) 4 104.3 9584511 105 94.4-112.9 1.20x -slotwise_mpsc 8 152.0 6577065 4472 150.8-159.0 1.05x -reserving_mpsc 8 159.6 6264859 2131 150.7-161.0 1.07x -permit_mpsc 8 142.6 7012992 585672 134.8-148.4 1.10x -reserving(32/32) 8 160.5 6230617 5597 154.7-178.9 1.16x -reserving(16/48) 8 161.7 6183623 2970 156.1-169.5 1.09x -reserving(8/56) 8 157.0 6369701 645 143.2-162.1 1.13x -reserving(64/64) 8 175.2 5708675 1471 165.2-178.0 1.08x -slotwise_mpsc 16 319.5 3130055 2493998 214.1-380.0 1.78x -reserving_mpsc 16 275.5 3629762 2111325 266.6-365.4 1.37x -permit_mpsc 16 291.4 3432079 3319000 235.6-312.6 1.33x -reserving(32/32) 16 299.9 3334441 2633072 248.6-348.8 1.40x -reserving(16/48) 16 285.1 3507777 1482100 225.9-295.6 1.31x -reserving(8/56) 16 321.4 3111441 2155304 222.5-353.7 1.59x -reserving(64/64) 16 305.5 3273403 2010188 267.7-348.8 1.30x -slotwise_mpsc 32 550.8 1815636 15636391 518.0-664.1 1.28x -reserving_mpsc 32 742.1 1347448 30259881 657.0-804.2 1.22x -permit_mpsc 32 500.8 1996881 11882456 367.0-540.3 1.47x -reserving(32/32) 32 687.7 1454211 26050632 495.1-989.3 2.00x -reserving(16/48) 32 789.2 1267080 29833589 614.3-1050.1 1.71x -reserving(8/56) 32 661.8 1511090 21873245 615.6-713.6 1.16x -reserving(64/64) 32 546.9 1828526 14442989 399.8-1187.3 2.97x +slotwise_mpsc 1 10.2 97675327 37 9.7-11.1 1.14x +reserving_mpsc 1 23.8 42052145 648 22.2-25.2 1.14x +permit_mpsc 1 59.7 16762211 487 57.3-68.7 1.20x +reserving(32/32) 1 29.1 34340659 0 27.7-32.4 1.17x +reserving(16/48) 1 28.5 35075412 450 23.9-30.3 1.27x +reserving(8/56) 1 26.8 37366415 1285 25.2-30.1 1.19x +reserving(64/64) 1 27.4 36456435 397 25.4-36.3 1.43x +slotwise_mpsc 2 72.2 13845621 322 69.4-80.0 1.15x +reserving_mpsc 2 68.4 14619028 7678 67.2-69.1 1.03x +permit_mpsc 2 54.7 18293241 43 53.5-55.0 1.03x +reserving(32/32) 2 64.6 15480595 2799 62.8-69.8 1.11x +reserving(16/48) 2 62.0 16128512 1640 59.7-69.8 1.17x +reserving(8/56) 2 71.3 14028000 206 65.7-74.5 1.13x +reserving(64/64) 2 69.6 14377732 66 62.3-73.9 1.19x +slotwise_mpsc 4 107.0 9344266 1293 97.7-109.3 1.12x +reserving_mpsc 4 97.9 10219097 1407 83.6-100.7 1.21x +permit_mpsc 4 71.7 13950504 61931 66.3-78.1 1.18x +reserving(32/32) 4 96.1 10405936 0 79.8-98.7 1.24x +reserving(16/48) 4 96.5 10363822 263 84.1-100.8 1.20x +reserving(8/56) 4 103.0 9707984 348 88.8-107.4 1.21x +reserving(64/64) 4 106.0 9435164 428 96.6-113.6 1.18x +slotwise_mpsc 8 146.6 6821375 2645 145.8-160.4 1.10x +reserving_mpsc 8 160.4 6236329 4435 154.3-170.8 1.11x +permit_mpsc 8 119.8 8345782 475034 96.8-150.6 1.56x +reserving(32/32) 8 173.6 5758926 8263 170.6-176.3 1.03x +reserving(16/48) 8 169.8 5888189 0 167.0-175.2 1.05x +reserving(8/56) 8 168.6 5930943 7305 161.6-173.8 1.08x +reserving(64/64) 8 179.6 5566937 7573 170.7-188.5 1.10x +slotwise_mpsc 16 329.5 3035177 2119296 274.9-353.4 1.29x +reserving_mpsc 16 233.1 4290545 738295 215.5-254.0 1.18x +permit_mpsc 16 219.5 4556538 2265316 197.9-333.9 1.69x +reserving(32/32) 16 308.8 3238569 3098327 201.4-327.2 1.62x +reserving(16/48) 16 264.9 3775086 1766041 211.4-428.0 2.02x +reserving(8/56) 16 285.8 3498926 2084768 241.0-346.0 1.44x +reserving(64/64) 16 246.4 4058429 1156019 215.3-307.4 1.43x +slotwise_mpsc 32 558.7 1790020 16403774 394.2-768.9 1.95x +reserving_mpsc 32 630.0 1587180 23588446 331.1-711.0 2.15x +permit_mpsc 32 531.5 1881620 12610199 419.9-644.1 1.53x +reserving(32/32) 32 510.4 1959073 16810770 395.0-627.1 1.59x +reserving(16/48) 32 640.9 1560202 21611025 518.3-903.3 1.74x +reserving(8/56) 32 677.9 1475132 22815835 527.2-956.4 1.81x +reserving(64/64) 32 568.1 1760288 15246419 501.2-631.0 1.26x interpretation: @@ -108,11 +108,11 @@ interpretation: producers slotwise reserving permit atomic floor 1 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] - 2 0.10x [0.09-0.12] 0.15x [0.15-0.17] 0.20x [0.18-0.35] 0.21x [0.14-0.37] - 4 0.07x [0.07-0.08] 0.15x [0.13-0.19] 0.26x [0.25-0.32] 0.15x [0.12-0.17] - 8 0.04x [0.04-0.05] 0.14x [0.11-0.15] 0.32x [0.29-0.40] 0.15x [0.12-0.17] - 16 0.03x [0.03-0.03] 0.11x [0.10-0.11] 0.40x [0.34-0.47] 0.16x [0.13-0.17] - 32 0.03x [0.03-0.03] 0.11x [0.10-0.12] 0.38x [0.36-0.44] 0.15x [0.13-0.16] + 2 0.10x [0.10-0.14] 0.15x [0.14-0.20] 0.19x [0.18-0.22] 0.19x [0.18-0.22] + 4 0.07x [0.06-0.07] 0.15x [0.14-0.17] 0.25x [0.25-0.28] 0.17x [0.15-0.20] + 8 0.04x [0.04-0.05] 0.14x [0.13-0.16] 0.31x [0.30-0.34] 0.15x [0.13-0.18] + 16 0.02x [0.02-0.03] 0.11x [0.10-0.12] 0.40x [0.35-0.44] 0.15x [0.15-0.18] + 32 0.03x [0.03-0.03] 0.10x [0.10-0.12] 0.37x [0.36-0.40] 0.15x [0.15-0.18] Read as: throughput at N producers divided by throughput at one. 1.00 means N threads together push no faster than one did. @@ -130,12 +130,12 @@ interpretation: producers slotwise reserving reserving/slotwise permit permit/reserving ns/op ns/op ratio [bound] ns/op ratio [bound] - 1 9.8 25.2 2.57x [2.15-3.06] 61.0 2.42x [2.15-2.64] - 2 77.5 69.2 0.89x [0.78-3.38] 54.3 0.79x [0.75-0.89] - 4 104.9 94.3 0.90x [0.77-1.15] 80.3 0.85x [0.49-0.95] - 8 152.0 159.6 1.05x [0.95-1.07] 142.6 0.89x [0.84-0.98] - 16 319.5 275.5 0.86x [0.70-1.71] 291.4 1.06x [0.64-1.17] - 32 550.8 742.1 1.35x [0.99-1.55] 500.8 0.67x [0.46-0.82] + 1 10.2 23.8 2.32x [1.99-2.60] 59.7 2.51x [2.27-3.10] + 2 72.2 68.4 0.95x [0.84-1.00] 54.7 0.80x [0.77-0.82] + 4 107.0 97.9 0.91x [0.76-1.03] 71.7 0.73x [0.66-0.93] + 8 146.6 160.4 1.09x [0.96-1.17] 119.8 0.75x [0.57-0.98] + 16 329.5 233.1 0.71x [0.61-0.92] 219.5 0.94x [0.78-1.55] + 32 558.7 630.0 1.13x [0.43-1.80] 531.5 0.84x [0.59-1.95] `reserving_mpsc` reads the consumer's position on every push and `slotwise_mpsc` does not. This regime is where that read is at its @@ -165,8 +165,8 @@ interpretation: difference between them is not thereby noise. Read it against a control before calling it either way: the reserving_mpsc row and the 32/32 row above are the same code, so the gap between them is - what 'no difference' looks like on this host -- which across seven - runs was not zero, and was wide enough to swallow the layout rows. + what 'no difference' looks like on this host -- read it against + the layout rows before calling any of them apart. 64/64 vs 32/32 is the double-width layout's effect on the whole push path -- what moving the recurrence to 2^64 costs, against 8/56 moving it to 2^56. Both defer the recurrence rather than @@ -174,21 +174,21 @@ interpretation: -- isolated -- producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs - 1 5.4 5.5 5.6 7.4 1.02x [0.93-1.03] 1.03x [0.93-1.11] 1.37x [1.29-1.43] - 2 32.5 34.9 33.2 38.6 1.08x [0.73-1.47] 1.02x [0.91-1.43] 1.19x [1.00-1.60] - 4 38.9 37.9 35.5 47.2 0.97x [0.89-1.08] 0.91x [0.83-1.13] 1.21x [1.01-1.51] - 8 42.4 53.3 37.6 71.6 1.26x [1.04-1.54] 0.89x [0.76-1.30] 1.69x [1.13-2.70] - 16 47.6 61.0 61.4 166.7 1.28x [1.21-1.48] 1.29x [1.11-1.50] 3.50x [2.61-4.39] - 32 50.0 62.9 53.2 201.4 1.26x [1.03-1.46] 1.06x [1.02-1.31] 4.03x [3.37-4.54] + 1 6.8 6.4 6.6 11.8 0.94x [0.68-1.26] 0.97x [0.66-1.25] 1.73x [1.11-2.21] + 2 36.0 33.6 32.8 39.5 0.94x [0.82-1.08] 0.91x [0.64-1.12] 1.10x [0.95-1.23] + 4 38.0 32.7 35.3 52.1 0.86x [0.76-1.03] 0.93x [0.76-1.19] 1.37x [1.06-1.67] + 8 38.2 36.4 43.6 78.3 0.95x [0.81-1.06] 1.14x [0.78-1.25] 2.05x [1.40-2.87] + 16 46.2 57.9 52.7 184.6 1.25x [1.18-1.36] 1.14x [1.06-1.29] 3.99x [2.82-4.22] + 32 52.3 74.5 66.0 185.3 1.42x [1.23-1.50] 1.26x [1.17-1.41] 3.55x [3.18-4.56] -- drained -- producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs - 1 26.3 28.0 27.9 29.8 1.06x [0.95-1.17] 1.06x [0.90-1.20] 1.13x [0.94-1.23] - 2 67.6 70.0 67.2 72.0 1.04x [0.93-1.13] 0.99x [0.93-1.12] 1.07x [0.99-1.16] - 4 94.0 95.6 99.4 104.3 1.02x [0.90-1.20] 1.06x [0.91-1.21] 1.11x [0.92-1.33] - 8 160.5 161.7 157.0 175.2 1.01x [0.87-1.10] 0.98x [0.80-1.05] 1.09x [0.92-1.15] - 16 299.9 285.1 321.4 305.5 0.95x [0.65-1.19] 1.07x [0.64-1.42] 1.02x [0.77-1.40] - 32 687.7 789.2 661.8 546.9 1.15x [0.62-2.12] 0.96x [0.62-1.44] 0.80x [0.40-2.40] + 1 29.1 28.5 26.8 27.4 0.98x [0.74-1.10] 0.92x [0.78-1.09] 0.94x [0.78-1.31] + 2 64.6 62.0 71.3 69.6 0.96x [0.85-1.11] 1.10x [0.94-1.19] 1.08x [0.89-1.18] + 4 96.1 96.5 103.0 106.0 1.00x [0.85-1.26] 1.07x [0.90-1.35] 1.10x [0.98-1.42] + 8 173.6 169.8 168.6 179.6 0.98x [0.95-1.03] 0.97x [0.92-1.02] 1.03x [0.97-1.10] + 16 308.8 264.9 285.8 246.4 0.86x [0.65-2.13] 0.93x [0.74-1.72] 0.80x [0.66-1.53] + 32 510.4 640.9 677.9 568.1 1.26x [0.83-2.29] 1.33x [0.84-2.42] 1.11x [0.80-1.60] the 32/32 row and the reserving_mpsc row above are the same configuration run twice, so the gap between them is this host's diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run3.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run3.txt index 9d688605d..4dd771606 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run3.txt +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/run3.txt @@ -8,99 +8,99 @@ sampling: 50000 pushes per producer, median of 5 repetitions, one untimed warmup -- isolated: producers only, capacity large enough that nothing is refused -- shape producers ns/op ops/sec refusals ns/op range spread -baseline_fetch_add 1 2.3 431778929 0 2.3-3.3 1.43x -slotwise_mpsc 1 6.1 163452109 0 6.0-6.4 1.07x -reserving_mpsc 1 5.4 183891136 0 5.4-5.7 1.05x -permit_mpsc 1 7.9 126742712 0 7.9-8.4 1.07x -reserving(32/32) 1 5.4 184979652 0 5.4-5.4 1.01x -reserving(16/48) 1 5.4 185253798 0 5.4-5.6 1.04x -reserving(8/56) 1 5.4 185459941 0 5.4-5.4 1.01x -reserving(64/64) 1 7.6 130855797 0 7.5-8.0 1.08x -baseline_fetch_add 2 11.7 85229694 0 9.9-11.9 1.20x -slotwise_mpsc 2 51.2 19515241 0 31.0-57.3 1.84x -reserving_mpsc 2 31.9 31332247 0 15.1-34.1 2.25x -permit_mpsc 2 41.0 24408103 0 37.6-41.8 1.11x -reserving(32/32) 2 34.8 28724902 0 32.6-36.6 1.12x -reserving(16/48) 2 34.4 29085832 0 31.0-36.8 1.19x -reserving(8/56) 2 30.9 32324800 0 27.0-35.1 1.30x -reserving(64/64) 2 38.2 26177325 0 23.8-39.1 1.64x -baseline_fetch_add 4 14.8 67553874 0 13.1-16.0 1.22x -slotwise_mpsc 4 90.2 11089487 0 83.8-92.7 1.11x -reserving_mpsc 4 34.7 28830907 0 32.8-36.9 1.13x -permit_mpsc 4 30.9 32372936 0 30.0-33.2 1.10x -reserving(32/32) 4 35.5 28170204 0 30.0-37.0 1.23x -reserving(16/48) 4 37.2 26877024 0 35.5-42.3 1.19x -reserving(8/56) 4 38.8 25759254 0 32.1-39.4 1.23x -reserving(64/64) 4 50.8 19699387 0 42.5-51.5 1.21x -baseline_fetch_add 8 15.2 65907630 0 14.3-15.8 1.11x -slotwise_mpsc 8 147.0 6801160 0 141.1-148.6 1.05x -reserving_mpsc 8 41.7 23998800 0 35.9-53.1 1.48x -permit_mpsc 8 25.1 39891097 0 25.0-25.8 1.03x -reserving(32/32) 8 41.9 23840744 0 39.7-45.4 1.14x -reserving(16/48) 8 42.5 23508254 0 37.4-47.4 1.27x -reserving(8/56) 8 34.8 28721395 0 33.1-47.8 1.44x -reserving(64/64) 8 79.3 12609148 0 77.3-88.4 1.14x -baseline_fetch_add 16 15.1 66413190 0 14.7-15.4 1.05x -slotwise_mpsc 16 208.7 4792399 0 189.7-231.5 1.22x -reserving_mpsc 16 50.3 19870840 0 45.8-52.7 1.15x -permit_mpsc 16 21.0 47640883 0 20.5-23.9 1.17x -reserving(32/32) 16 45.9 21766990 0 40.7-51.8 1.27x -reserving(16/48) 16 65.0 15374031 0 58.8-67.8 1.15x -reserving(8/56) 16 57.0 17551057 0 52.1-62.6 1.20x -reserving(64/64) 16 175.5 5697628 0 134.9-194.4 1.44x -baseline_fetch_add 32 15.0 66880405 0 14.7-15.5 1.05x -slotwise_mpsc 32 252.5 3960634 0 202.8-257.8 1.27x -reserving_mpsc 32 49.3 20274183 0 46.0-51.0 1.11x -permit_mpsc 32 22.4 44708598 0 22.0-22.6 1.03x -reserving(32/32) 32 49.0 20420588 0 47.8-51.7 1.08x -reserving(16/48) 32 63.1 15840816 0 61.9-72.4 1.17x -reserving(8/56) 32 63.8 15686075 0 61.5-69.6 1.13x -reserving(64/64) 32 173.8 5754768 0 152.3-195.8 1.29x +baseline_fetch_add 1 2.3 432152118 0 2.3-2.6 1.13x +slotwise_mpsc 1 6.2 161082474 0 5.9-6.4 1.09x +reserving_mpsc 1 5.7 175377061 0 5.4-6.3 1.15x +permit_mpsc 1 8.0 125031258 0 7.9-8.4 1.07x +reserving(32/32) 1 5.4 184774575 0 5.4-5.8 1.08x +reserving(16/48) 1 5.4 186706497 0 5.3-5.7 1.08x +reserving(8/56) 1 5.4 185597624 0 5.4-5.7 1.06x +reserving(64/64) 1 7.5 134120172 0 7.4-7.7 1.03x +baseline_fetch_add 2 12.2 82209799 0 10.8-12.7 1.17x +slotwise_mpsc 2 56.8 17613076 0 54.0-57.6 1.07x +reserving_mpsc 2 35.1 28489217 0 24.2-35.6 1.47x +permit_mpsc 2 42.5 23510051 0 41.0-43.1 1.05x +reserving(32/32) 2 36.5 27398762 0 33.5-37.7 1.13x +reserving(16/48) 2 35.5 28190455 0 33.7-37.1 1.10x +reserving(8/56) 2 33.0 30335204 0 32.0-36.8 1.15x +reserving(64/64) 2 40.2 24857690 0 39.2-41.1 1.05x +baseline_fetch_add 4 14.9 66961296 0 13.7-15.7 1.14x +slotwise_mpsc 4 87.5 11426156 0 84.5-89.6 1.06x +reserving_mpsc 4 39.2 25497520 0 37.0-39.5 1.07x +permit_mpsc 4 30.8 32489157 0 30.3-31.5 1.04x +reserving(32/32) 4 37.1 26948004 0 33.8-37.4 1.11x +reserving(16/48) 4 35.6 28082394 0 35.2-36.1 1.03x +reserving(8/56) 4 36.7 27236453 0 35.0-38.3 1.10x +reserving(64/64) 4 50.6 19743726 0 45.6-56.3 1.23x +baseline_fetch_add 8 15.8 63281126 0 15.2-16.6 1.09x +slotwise_mpsc 8 133.8 7474428 0 125.0-140.7 1.13x +reserving_mpsc 8 46.9 21300729 0 39.4-48.5 1.23x +permit_mpsc 8 24.2 41306099 0 22.1-24.4 1.10x +reserving(32/32) 8 42.0 23781213 0 35.3-43.3 1.23x +reserving(16/48) 8 46.6 21479970 0 37.5-53.9 1.43x +reserving(8/56) 8 37.0 26998022 0 36.4-44.9 1.23x +reserving(64/64) 8 70.1 14259030 0 59.9-71.5 1.19x +baseline_fetch_add 16 15.1 66365808 0 14.9-15.1 1.02x +slotwise_mpsc 16 202.0 4950869 0 171.0-242.4 1.42x +reserving_mpsc 16 52.6 19004768 0 46.4-55.2 1.19x +permit_mpsc 16 22.3 44793084 0 20.0-22.8 1.14x +reserving(32/32) 16 46.3 21597048 0 40.2-47.3 1.18x +reserving(16/48) 16 59.5 16805664 0 55.8-60.6 1.09x +reserving(8/56) 16 59.0 16961658 0 52.9-61.1 1.15x +reserving(64/64) 16 182.6 5476841 0 148.8-228.1 1.53x +baseline_fetch_add 32 14.7 68208753 0 14.6-15.1 1.03x +slotwise_mpsc 32 258.7 3865605 0 211.5-264.6 1.25x +reserving_mpsc 32 47.6 21022619 0 47.1-51.5 1.09x +permit_mpsc 32 22.0 45478060 0 21.2-22.2 1.05x +reserving(32/32) 32 50.6 19763773 0 46.8-53.2 1.14x +reserving(16/48) 32 68.4 14622341 0 67.9-75.9 1.12x +reserving(8/56) 32 73.3 13650386 0 65.8-77.5 1.18x +reserving(64/64) 32 241.4 4141800 0 191.4-245.5 1.28x -- drained: a consumer popping continuously, capacity 1024 -- shape producers ns/op ops/sec refusals ns/op range spread -slotwise_mpsc 1 10.7 93861460 1482 9.7-11.1 1.15x -reserving_mpsc 1 25.2 39739310 243 24.6-31.0 1.26x -permit_mpsc 1 62.1 16091140 506 58.6-62.8 1.07x -reserving(32/32) 1 24.8 40397512 977 21.0-26.9 1.28x -reserving(16/48) 1 28.2 35463508 758 27.2-32.2 1.19x -reserving(8/56) 1 28.8 34693311 0 23.8-33.4 1.40x -reserving(64/64) 1 27.8 35955703 1114 27.1-33.7 1.25x -slotwise_mpsc 2 65.2 15331075 560 49.7-70.0 1.41x -reserving_mpsc 2 68.6 14582999 29 65.2-70.7 1.08x -permit_mpsc 2 52.5 19030944 585 50.8-56.8 1.12x -reserving(32/32) 2 67.3 14864142 308 65.5-70.5 1.08x -reserving(16/48) 2 66.9 14943662 0 62.8-68.5 1.09x -reserving(8/56) 2 66.3 15085914 756 65.5-69.8 1.06x -reserving(64/64) 2 68.4 14616250 791 46.7-70.9 1.52x -slotwise_mpsc 4 99.5 10048181 0 94.4-111.2 1.18x -reserving_mpsc 4 89.4 11188436 6870 78.3-98.7 1.26x -permit_mpsc 4 77.8 12851323 86633 68.9-80.4 1.17x -reserving(32/32) 4 96.7 10343615 185 94.9-101.9 1.07x -reserving(16/48) 4 93.7 10669853 15608 82.4-99.8 1.21x -reserving(8/56) 4 97.7 10234419 0 91.0-99.0 1.09x -reserving(64/64) 4 106.3 9406010 0 100.7-125.6 1.25x -slotwise_mpsc 8 149.5 6688057 4512 144.5-156.5 1.08x -reserving_mpsc 8 162.5 6153174 1594 155.1-164.2 1.06x -permit_mpsc 8 142.1 7036790 594273 122.2-152.3 1.25x -reserving(32/32) 8 162.5 6154926 1295 160.3-166.4 1.04x -reserving(16/48) 8 165.0 6060496 220 148.6-168.6 1.13x -reserving(8/56) 8 161.1 6205639 15873 150.5-169.0 1.12x -reserving(64/64) 8 172.1 5810212 2748 153.0-177.0 1.16x -slotwise_mpsc 16 249.0 4016470 664737 239.4-335.5 1.40x -reserving_mpsc 16 270.7 3694365 1613977 263.2-391.1 1.49x -permit_mpsc 16 265.0 3774174 2932676 209.5-315.4 1.51x -reserving(32/32) 16 264.7 3777680 1609361 213.5-333.8 1.56x -reserving(16/48) 16 266.3 3755021 1371904 243.7-312.5 1.28x -reserving(8/56) 16 278.9 3585232 1373700 260.9-307.7 1.18x -reserving(64/64) 16 241.0 4148807 570234 213.1-288.0 1.35x -slotwise_mpsc 32 537.5 1860401 15048333 484.4-658.6 1.36x -reserving_mpsc 32 609.1 1641797 22486824 484.0-802.9 1.66x -permit_mpsc 32 491.5 2034595 11829136 405.7-647.8 1.60x -reserving(32/32) 32 595.0 1680775 21516893 527.5-787.1 1.49x -reserving(16/48) 32 573.8 1742909 19007483 519.6-797.0 1.53x -reserving(8/56) 32 628.4 1591406 20968836 569.1-889.8 1.56x -reserving(64/64) 32 654.3 1528245 19098846 451.5-670.6 1.49x +slotwise_mpsc 1 10.7 93370682 40 8.7-11.1 1.27x +reserving_mpsc 1 23.1 43230157 225 22.3-24.9 1.11x +permit_mpsc 1 62.2 16065806 195 59.9-63.1 1.05x +reserving(32/32) 1 24.0 41673612 628 21.9-25.0 1.15x +reserving(16/48) 1 26.8 37302298 1161 25.9-27.8 1.07x +reserving(8/56) 1 27.0 37100245 1419 25.0-31.5 1.26x +reserving(64/64) 1 29.1 34385531 679 27.6-31.2 1.13x +slotwise_mpsc 2 70.3 14227584 605 67.7-77.1 1.14x +reserving_mpsc 2 63.1 15853891 2858 62.1-66.7 1.07x +permit_mpsc 2 54.2 18465857 52 52.3-54.5 1.04x +reserving(32/32) 2 68.6 14582786 995 67.3-69.1 1.03x +reserving(16/48) 2 67.2 14874091 847 66.1-69.6 1.05x +reserving(8/56) 2 67.4 14839217 169 64.4-72.4 1.13x +reserving(64/64) 2 69.7 14355647 44 69.3-71.2 1.03x +slotwise_mpsc 4 103.8 9637300 1991 96.4-106.2 1.10x +reserving_mpsc 4 91.4 10938765 2017 83.2-97.2 1.17x +permit_mpsc 4 73.6 13587695 78415 61.2-89.6 1.46x +reserving(32/32) 4 96.4 10370916 8168 93.5-101.9 1.09x +reserving(16/48) 4 94.2 10610248 2135 92.6-96.5 1.04x +reserving(8/56) 4 94.4 10593669 917 80.2-101.5 1.27x +reserving(64/64) 4 92.4 10827960 3433 89.0-98.8 1.11x +slotwise_mpsc 8 151.3 6607748 2909 146.2-153.3 1.05x +reserving_mpsc 8 157.2 6360241 1794 153.3-164.1 1.07x +permit_mpsc 8 131.6 7597529 553659 122.3-148.6 1.22x +reserving(32/32) 8 160.2 6241428 4894 156.0-176.2 1.13x +reserving(16/48) 8 157.6 6343387 4510 152.0-174.4 1.15x +reserving(8/56) 8 163.7 6110257 15254 142.7-173.8 1.22x +reserving(64/64) 8 170.3 5871422 0 155.7-180.1 1.16x +slotwise_mpsc 16 295.5 3384563 1724125 198.4-346.7 1.75x +reserving_mpsc 16 268.5 3723944 1563173 193.3-334.3 1.73x +permit_mpsc 16 272.8 3665271 3043100 196.5-310.3 1.58x +reserving(32/32) 16 306.4 3263624 2683848 234.3-359.3 1.53x +reserving(16/48) 16 278.7 3587611 1198515 227.4-304.6 1.34x +reserving(8/56) 16 272.1 3675748 960270 234.4-365.6 1.56x +reserving(64/64) 16 237.5 4210548 31703 225.1-314.8 1.40x +slotwise_mpsc 32 635.7 1573041 19058153 432.0-717.3 1.66x +reserving_mpsc 32 675.4 1480673 25956490 475.2-768.1 1.62x +permit_mpsc 32 431.6 2316787 10629824 329.4-547.5 1.66x +reserving(32/32) 32 691.7 1445741 26425860 492.6-814.3 1.65x +reserving(16/48) 32 633.9 1577466 21810973 619.2-949.4 1.53x +reserving(8/56) 32 829.0 1206298 28202713 570.4-956.6 1.68x +reserving(64/64) 32 647.7 1544019 19247556 531.5-718.7 1.35x interpretation: @@ -108,11 +108,11 @@ interpretation: producers slotwise reserving permit atomic floor 1 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] 1.00x [1.00-1.00] - 2 0.12x [0.10-0.21] 0.17x [0.16-0.38] 0.19x [0.19-0.22] 0.20x [0.19-0.33] - 4 0.07x [0.06-0.08] 0.16x [0.15-0.17] 0.26x [0.24-0.28] 0.16x [0.14-0.25] - 8 0.04x [0.04-0.05] 0.13x [0.10-0.16] 0.31x [0.31-0.34] 0.15x [0.15-0.23] - 16 0.03x [0.03-0.03] 0.11x [0.10-0.12] 0.38x [0.33-0.41] 0.15x [0.15-0.22] - 32 0.02x [0.02-0.03] 0.11x [0.11-0.12] 0.35x [0.35-0.38] 0.15x [0.15-0.22] + 2 0.11x [0.10-0.12] 0.16x [0.15-0.26] 0.19x [0.18-0.20] 0.19x [0.18-0.24] + 4 0.07x [0.07-0.08] 0.15x [0.14-0.17] 0.26x [0.25-0.28] 0.15x [0.15-0.19] + 8 0.05x [0.04-0.05] 0.12x [0.11-0.16] 0.33x [0.32-0.38] 0.15x [0.14-0.17] + 16 0.03x [0.02-0.04] 0.11x [0.10-0.14] 0.36x [0.35-0.42] 0.15x [0.15-0.17] + 32 0.02x [0.02-0.03] 0.12x [0.11-0.13] 0.36x [0.35-0.39] 0.16x [0.15-0.18] Read as: throughput at N producers divided by throughput at one. 1.00 means N threads together push no faster than one did. @@ -130,12 +130,12 @@ interpretation: producers slotwise reserving reserving/slotwise permit permit/reserving ns/op ns/op ratio [bound] ns/op ratio [bound] - 1 10.7 25.2 2.36x [2.21-3.21] 62.1 2.47x [1.89-2.55] - 2 65.2 68.6 1.05x [0.93-1.42] 52.5 0.77x [0.72-0.87] - 4 99.5 89.4 0.90x [0.70-1.05] 77.8 0.87x [0.70-1.03] - 8 149.5 162.5 1.09x [0.99-1.14] 142.1 0.87x [0.74-0.98] - 16 249.0 270.7 1.09x [0.78-1.63] 265.0 0.98x [0.54-1.20] - 32 537.5 609.1 1.13x [0.73-1.66] 491.5 0.81x [0.51-1.34] + 1 10.7 23.1 2.16x [2.01-2.85] 62.2 2.69x [2.41-2.83] + 2 70.3 63.1 0.90x [0.81-0.99] 54.2 0.86x [0.78-0.88] + 4 103.8 91.4 0.88x [0.78-1.01] 73.6 0.81x [0.63-1.08] + 8 151.3 157.2 1.04x [1.00-1.12] 131.6 0.84x [0.75-0.97] + 16 295.5 268.5 0.91x [0.56-1.68] 272.8 1.02x [0.59-1.61] + 32 635.7 675.4 1.06x [0.66-1.78] 431.6 0.64x [0.43-1.15] `reserving_mpsc` reads the consumer's position on every push and `slotwise_mpsc` does not. This regime is where that read is at its @@ -165,8 +165,8 @@ interpretation: difference between them is not thereby noise. Read it against a control before calling it either way: the reserving_mpsc row and the 32/32 row above are the same code, so the gap between them is - what 'no difference' looks like on this host -- which across seven - runs was not zero, and was wide enough to swallow the layout rows. + what 'no difference' looks like on this host -- read it against + the layout rows before calling any of them apart. 64/64 vs 32/32 is the double-width layout's effect on the whole push path -- what moving the recurrence to 2^64 costs, against 8/56 moving it to 2^56. Both defer the recurrence rather than @@ -174,21 +174,21 @@ interpretation: -- isolated -- producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs - 1 5.4 5.4 5.4 7.6 1.00x [0.99-1.04] 1.00x [0.99-1.01] 1.41x [1.37-1.49] - 2 34.8 34.4 30.9 38.2 0.99x [0.85-1.13] 0.89x [0.74-1.08] 1.10x [0.65-1.20] - 4 35.5 37.2 38.8 50.8 1.05x [0.96-1.41] 1.09x [0.87-1.31] 1.43x [1.15-1.72] - 8 41.9 42.5 34.8 79.3 1.01x [0.82-1.19] 0.83x [0.73-1.20] 1.89x [1.70-2.22] - 16 45.9 65.0 57.0 175.5 1.42x [1.14-1.67] 1.24x [1.01-1.54] 3.82x [2.61-4.78] - 32 49.0 63.1 63.8 173.8 1.29x [1.20-1.51] 1.30x [1.19-1.46] 3.55x [2.95-4.10] + 1 5.4 5.4 5.4 7.5 0.99x [0.92-1.06] 1.00x [0.93-1.06] 1.38x [1.28-1.42] + 2 36.5 35.5 33.0 40.2 0.97x [0.89-1.11] 0.90x [0.85-1.10] 1.10x [1.04-1.23] + 4 37.1 35.6 36.7 50.6 0.96x [0.94-1.07] 0.99x [0.93-1.13] 1.36x [1.22-1.66] + 8 42.0 46.6 37.0 70.1 1.11x [0.87-1.52] 0.88x [0.84-1.27] 1.67x [1.38-2.02] + 16 46.3 59.5 59.0 182.6 1.29x [1.18-1.51] 1.27x [1.12-1.52] 3.94x [3.15-5.67] + 32 50.6 68.4 73.3 241.4 1.35x [1.28-1.62] 1.45x [1.24-1.66] 4.77x [3.60-5.25] -- drained -- producers 32/32 ns/op 16/48 ns/op 8/56 ns/op 64/64 ns/op 16/48 vs 8/56 vs 64/64 vs - 1 24.8 28.2 28.8 27.8 1.14x [1.01-1.54] 1.16x [0.89-1.59] 1.12x [1.01-1.61] - 2 67.3 66.9 66.3 68.4 0.99x [0.89-1.05] 0.99x [0.93-1.07] 1.02x [0.66-1.08] - 4 96.7 93.7 97.7 106.3 0.97x [0.81-1.05] 1.01x [0.89-1.04] 1.10x [0.99-1.32] - 8 162.5 165.0 161.1 172.1 1.02x [0.89-1.05] 0.99x [0.90-1.05] 1.06x [0.92-1.10] - 16 264.7 266.3 278.9 241.0 1.01x [0.73-1.46] 1.05x [0.78-1.44] 0.91x [0.64-1.35] - 32 595.0 573.8 628.4 654.3 0.96x [0.66-1.51] 1.06x [0.72-1.69] 1.10x [0.57-1.27] + 1 24.0 26.8 27.0 29.1 1.12x [1.04-1.27] 1.12x [1.00-1.44] 1.21x [1.10-1.43] + 2 68.6 67.2 67.4 69.7 0.98x [0.96-1.03] 0.98x [0.93-1.08] 1.02x [1.00-1.06] + 4 96.4 94.2 94.4 92.4 0.98x [0.91-1.03] 0.98x [0.79-1.09] 0.96x [0.87-1.06] + 8 160.2 157.6 163.7 170.3 0.98x [0.86-1.12] 1.02x [0.81-1.11] 1.06x [0.88-1.15] + 16 306.4 278.7 272.1 237.5 0.91x [0.63-1.30] 0.89x [0.65-1.56] 0.78x [0.63-1.34] + 32 691.7 633.9 829.0 647.7 0.92x [0.76-1.93] 1.20x [0.70-1.94] 0.94x [0.65-1.46] the 32/32 row and the reserving_mpsc row above are the same configuration run twice, so the gap between them is this host's diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 7f81b025d..967bdff65 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -1,3 +1,4 @@ +// Copyright (c) Mike Grier. // Summarise the drained tables of a queue-contention capture. // // Reads the probe's own report text rather than re-deriving anything: the diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt index ecd883f89..6d0badfc1 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt @@ -2,17 +2,17 @@ runs: 3 drained layout ratios vs 32/32, median across runs producers 16/48 8/56 64/64 - 1 1.07x 1.06x 1.12x - 2 0.99x 0.99x 1.07x - 4 1.02x 1.06x 1.11x - 8 1.02x 0.98x 1.06x - 16 1.01x 1.05x 1.02x - 32 0.96x 0.96x 1.02x + 1 1.01x 1.03x 1.17x + 2 0.98x 1.03x 1.06x + 4 1.00x 1.00x 1.09x + 8 0.98x 1.01x 1.06x + 16 0.91x 0.93x 0.80x + 32 1.23x 1.20x 0.94x same-code control (reserving_mpsc vs reserving 32/32), drained observations: 18 - span: 0.92x to 1.25x - median: 1.01x + span: 0.75x to 1.23x + median: 0.98x -layout medians: 18, spanning 0.96x to 1.12x +layout medians: 18, spanning 0.80x to 1.23x every layout median inside the control band: true diff --git a/crates/windows-platform-probes/src/report.rs b/crates/windows-platform-probes/src/report.rs index 6fa4cbf91..ca7807a59 100644 --- a/crates/windows-platform-probes/src/report.rs +++ b/crates/windows-platform-probes/src/report.rs @@ -94,12 +94,13 @@ impl Captured { /// /// This is the answer to "how does a formatted line reach the sink", and the /// reason it is a [`std::fmt::Write`] adapter rather than a method on [`Report`] -/// is a property rather than a count. Every renderer writes through +/// is a property rather than a count. Every renderer already wrote through /// `writeln!(out, ...)` against a `String`; a sink method taking /// `fmt::Arguments` would have been explicit but would have rewritten every one /// of those sites, while `String` already implements `fmt::Write` -- so a sink -/// that does too lets every write site stand untouched and moves only the -/// renderer signatures. +/// that does too let every write site stand untouched and moved only the +/// renderer signatures, which now take `&mut dyn fmt::Write` and are handed a +/// [`LineSink`] by `emit_report_to`. /// /// The census that decided it is recorded in /// [DESIGN-NOTES.md](../DESIGN-NOTES.md#d-streaming-report) rather than repeated From 9df9cff11eca20f8b337e8cc8073de8a670e756a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 18:18:10 -0400 Subject: [PATCH 085/139] docs: stop citing one shape's rate as another's bound, and four smaller fixes From the same review round. The substantive one is an inversion I introduced last round. **A rate borrowed from `reserving_mpsc` was called a floor for shapes that measure faster than it.** `slotwise_mpsc` and `permit_mpsc` both described their 32-bit lap interval as "a floor, since that rate overstates throughput". The crate's own published table has `slotwise_mpsc` at 6.3 ns/op at one producer -- about 159M ops/s against the 116M/s reference -- so for that shape the wrap arrives *sooner* than the arithmetic says, not later. The direction was backwards. Both now name it as an arithmetic input taken from another shape rather than as a bound on this one. That is the same defect the round before last fixed in the opposite direction: the qualifier was wrong, and correcting it introduced a different wrong qualifier rather than removing the claim. Also: - The capture README still said producers "cannot" start before the consumer's first pop. `68198359` made that nearly true and the README now states the guarantee exactly, matching the rustdoc: no producer begins timing until the consumer has executed its pop path once, with continuous draining explicitly not guaranteed. - `Balanced`'s rustdoc still judged its own division ("not because the division is a good one ... the exposure is what pays for it"). D-41 declines to evaluate layouts; the factual ceiling and exposure stay, the verdict goes. - `DESIGN-RATIONALE.md` reintroduced the control span and a derived percentage two paragraphs after saying the span is recorded in the design note rather than here. Now qualitative, which is what that paragraph was arguing anyway. - Four archive cross-references made clickable, and two incidental test counts dropped from entries this branch wrote -- both violations of the rule the same entries document. Queues `M4.7`: the report renderer measures inside itself, so the only way to exercise it is the full 65-second run. Two defects shipped through that gap on this branch -- the hard-coded column widths and the seven-run footer -- which is the argument for the item and the reason it is recorded rather than left as a remark in a review thread. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 27 +++++++++++++++++++ .../COMPLETED-CHECKLIST.md | 16 +++++------ .../DESIGN-RATIONALE.md | 7 ++--- .../2026-09-16-drained-handshake/README.md | 7 +++-- .../src/permit_mpsc.rs | 6 ++--- .../src/reserving_mpsc.rs | 8 +++--- .../src/slotwise_mpsc.rs | 14 ++++++---- 7 files changed, 60 insertions(+), 25 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index f9141e447..1f9e18a21 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -191,6 +191,33 @@ correctness in the archive. explicitly after being pointed at the question. Nothing in the suite decides it either way, which is itself the argument for making it an invariant rather than a test. +- [ ] **M4.7** -- Make the queue-contention report renderer testable, by taking the observation as an + argument instead of measuring inside it. + + **Gap:** `render` in [src/bin/queue_contention.rs](src/bin/queue_contention.rs) calls `measure()` + itself, so the only way to exercise it is to run the whole ~65-second host-dependent measurement. + Everything it does beyond the library's `render_table` is therefore unreached by the suite: the + three tables' assembly, the derived column widths, the `cfg`-dependent `Wide` rows, and the + interpretation text between them. + + **This is not hypothetical, and that is the argument for the item.** Two defects shipped through + exactly this gap on the branch that wrote it. The scaling and drained-comparison tables hard-coded + a column width of 22 for formatters whose output has no fixed maximum, so a wide cell silently + pushed later columns out of line with their headers; and the drained footer printed a seven-run + control result beneath a table produced by a single invocation. Both were found by reading. A + fixture over the renderer would have caught the first mechanically and made the second visible. + + **Target:** a sibling that takes `&Observation` -- `render` keeps its signature and calls it with + `measure()`, so the binary's behaviour is unchanged -- and fixture-based tests over synthetic + observations. [src/topology_report.rs](src/topology_report.rs) is the precedent in this crate: + rendering moved into the library precisely so fixture observations could drive it. Note the test + file cannot be an inline `mod tests` per the repository's Rust rules, and a binary needs the + `src/bin//main.rs` layout to carry a sibling `tests.rs`; + `windows-placement-probe`'s `src/bin/placement_probe/` is the worked example. + + Reported by review against this branch, and queued rather than taken because it is a refactor of + the report's structure at a point where the branch is converging, not a defect. + - [ ] **M2.15** -- Run the probe suite on a second architecture in CI. **Keeps its conclusion but loses its evidence.** The five failures cited below were all diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 26446060e..f38b0f879 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1145,8 +1145,8 @@ reassuring cell the column can hold, produced by a row that measured nothing. `s original wording is noted rather than quietly replaced because it did not merely go stale: it presented returning zero as the fix, when returning zero was the defect.)* -Nine tests cover it, verified load-bearing by sabotage: taking the fastest from the median index -instead of the minimum fails `median_run_carries_the_fastest_and_slowest_repetitions`. +Verified load-bearing by sabotage: taking the fastest from the median index instead of the minimum +fails `median_run_carries_the_fastest_and_slowest_repetitions`. The dispersion justified itself on first capture. `slotwise_mpsc` at two producers spans 19.3 to 59.5 ns/op within one configuration on one host, which the median alone had @@ -1177,8 +1177,8 @@ instead of unblocking it. Applied to all nine timers at once, since they share the defect and this branch had three times shipped a correct fix applied to a subset of its call sites. -**Two of the four new tests were first written to assert on the test thread, and sabotage caught -both**: with `release` neutered they parked the test rather than failing it, which would wedge a +**Some of the new gate tests were first written to assert on the test thread, and sabotage caught +them**: with `release` neutered they parked the test rather than failing it, which would wedge a suite that runs its tests as threads in one process. Every gate test now arrives off-thread and polls, so a regression reddens in five seconds. @@ -1194,7 +1194,7 @@ hold until they see that before starting their clocks. Applied to all four drain **The blocker recorded when this was queued was real, and it is what made the item large.** The change moves the drained numbers, so every drained figure already published measured a different piece of code. Re-measured on the same host, three whole-probe invocations, committed as a capture -at `captures/2026-09-16-drained-handshake/` with the summarising script beside the raw runs so the +at [captures/2026-09-16-drained-handshake/](captures/2026-09-16-drained-handshake/README.md) with the summarising script beside the raw runs so the derivation can be checked rather than trusted. **The figures are amended rather than replaced**, because this is new data and not a correction: the @@ -1207,7 +1207,7 @@ not guaranteed: the drained conclusion did not depend on the window it had been ### M2.16 -- Repair the garbled `Report` doc comment, and drop the two counts that had rotted beside it. *(completed 2026-09-16 17:31:31 UTC-04:00)* -`src/report.rs` opened its `Report` sink doc with a dangling fragment -- a title line, a blank line, +[src/report.rs](src/report.rs) opened its `Report` sink doc with a dangling fragment -- a title line, a blank line, then "is arithmetic. Every renderer writes through ..." -- and further down repeated the bare word "signatures." after the sentence that already ended in it. Both were introduced on 2026-09-09. @@ -1221,11 +1221,11 @@ argument for CONTRACT INTEGRITY rule 4 as this repository has produced. The two homes were fixed together, because fixing one alone would have created a fresh disagreement: -- **`src/report.rs`** states the property instead of the count. `String` already implements +- **[src/report.rs](src/report.rs)** states the property instead of the count. `String` already implements `fmt::Write`, so a sink that does too leaves every write site untouched and moves only the renderer signatures. That is what makes the point, and it cannot rot. The lost sentence is restored, the duplicated word removed, and the census delegated to the design note by link. -- **`DESIGN-NOTES.md`** keeps the numbers, because there they *are* the finding: the decision was +- **[DESIGN-NOTES.md](DESIGN-NOTES.md)** keeps the numbers, because there they *are* the finding: the decision was taken by counting, and the entry contrasts M1's estimate of "upwards of 160" with what re-measuring found. They are now pinned as the census *as it stood on 2026-09-09 when the decision was taken* rather than stated in the present tense as a description of the crate now, and the passage no longer diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 2c728be90..7c919e61d 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -596,11 +596,12 @@ control the probe derives rather than asserts, built from two rows that are the same code at the same layout. It immediately did its job, withdrawing a claim that had survived several reviews. -It also very nearly produced a second error. Having found that the control spans -0.68-1.27x, the natural next move is to use it: judge every ratio against that +It also very nearly produced a second error. Having measured the control's span, +the natural next move is to use it: judge every ratio against that band, mark what falls outside, and report the result. That is what the first draft of the section did. But two measurements of identical code in the same run -differing by 27% is not a fact about the queue at all -- it is the instrument +differing by as much as they did is not a fact about the queue at all -- it is +the instrument telling you something, and using it as a ruler while declining to ask why it is elastic is how a methodological problem becomes permanent. The control had been promoted from *symptom* to *tool* without anyone deciding to do that. diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md index 0e3a51d35..c61632b40 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md @@ -6,8 +6,11 @@ say? **This is a second capture, not a replacement for the first.** The figures in [DESIGN-NOTES.md](../../DESIGN-NOTES.md) that predate `M4.3` measured a probe -whose producers could begin pushing before the consumer reached its first `pop`. -These measured a probe where they cannot. Both are real measurements; they are +whose producers could begin pushing before the consumer had run at all. These +measured a probe where no producer begins timing until the consumer has executed +its pop path at least once. That is the guarantee, stated exactly: continuous +draining is not guaranteed and no flag could express it, since the consumer can +be descheduled afterwards as it can at any point in the run. Both are real measurements of two different pieces of code, so they are kept side by side and each is labelled with the instrument that produced it. diff --git a/crates/windows-waitable-queues/src/permit_mpsc.rs b/crates/windows-waitable-queues/src/permit_mpsc.rs index d4385168f..9bfa6c14a 100644 --- a/crates/windows-waitable-queues/src/permit_mpsc.rs +++ b/crates/windows-waitable-queues/src/permit_mpsc.rs @@ -78,9 +78,9 @@ use crate::metrics::Metrics; /// /// **64 bits on every target, deliberately, rather than `usize`**, for the same /// reason [`slotwise_mpsc`](crate::slotwise_mpsc) made the same choice: a -/// 32-bit counter laps in minutes at the pre-correction planning rate -/// [`reserving_mpsc::ClaimLayout`] documents (a floor, since that rate -/// overstates throughput), and a shape +/// 32-bit counter laps in minutes at the reference rate +/// [`reserving_mpsc::ClaimLayout`] documents -- an arithmetic input taken from +/// another shape rather than a bound on this one -- and a shape /// whose soundness depends on the target's pointer width is not one this crate /// ships twice over. /// diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index c74f42c2c..036417cf0 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -558,10 +558,10 @@ impl ClaimWord for u128 { /// or more producers, the queue can **silently lose an item**: that is the whole /// of the `SH-14.1` exposure, and this layout carries it. /// -/// It is the default because it is what the shape shipped with, not because the -/// division is a good one: the reservation field it buys is far -/// beyond any use this crate has seen -- and beyond what its own capacity -/// permits -- while the exposure is what pays for it. +/// It is the default because it is what the shape shipped with. The reservation +/// field it buys is far beyond any use this crate has seen -- and beyond what +/// its own capacity permits -- while the position half is what sets the +/// exposure. /// [`Enduring`] and [`Perpetual`] spend that field the other way -- /// [`Enduring`] holds up to 65,535 outstanding reservations, [`Perpetual`] up to /// 255 -- each reachable only when the queue's capacity is at least that diff --git a/crates/windows-waitable-queues/src/slotwise_mpsc.rs b/crates/windows-waitable-queues/src/slotwise_mpsc.rs index 9d45e87b0..2c6717800 100644 --- a/crates/windows-waitable-queues/src/slotwise_mpsc.rs +++ b/crates/windows-waitable-queues/src/slotwise_mpsc.rs @@ -84,9 +84,12 @@ use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; /// counter cannot lap. /// /// With `usize` it can. On a 32-bit target the counter laps after 2^32 claims, -/// which at the pre-correction planning rate [`reserving_mpsc::ClaimLayout`] -/// documents is a matter of minutes -- a floor, since that rate overstates -/// throughput: the stalled +/// which is a matter of minutes at the reference rate +/// [`reserving_mpsc::ClaimLayout`] documents. **That rate is `reserving_mpsc`'s, +/// and is used here only as an arithmetic input rather than as a bound on this +/// shape**: this shape's own measured throughput differs, and at low producer +/// counts exceeds it, so the interval is neither a floor nor a forecast for it. +/// The stalled /// producer then sees the same tail bits, succeeds, and writes a slot that has /// since been refilled from the previous lap of the ring. Every other guard in /// this shape holds -- the position really is claimed by exactly one producer; @@ -554,8 +557,9 @@ impl Producer { // asked for the answer. // // Note what this property does *not* buy: measurement found this shape - // slower than `reserving_mpsc` under contention despite it. Why is not - // established -- the probe times the complete push, so the sequence read + // slower than `reserving_mpsc` under contention despite it. Why that is + // so is not established -- the probe times the complete push, so the + // sequence read // is one term among several and is never isolated. An earlier version of // this comment attributed it to the slot sequence marching through // memory while other producers write it; that mechanism is plausible and From d4f0bf8646ba16cfceaad1c0023f47d4aab6df86 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 18:31:57 -0400 Subject: [PATCH 086/139] test(probes): drive the report renderer from a corpus Completed item: M4.7: Make the queue-contention report renderer testable, by taking the observation as an argument instead of measuring inside it. `render` called `measure()` itself, so exercising it meant a ~65-second host-dependent pass, and everything beyond the library's `render_table` was reached by nothing in the suite. Two defects shipped through that gap on this branch: two tables hard-coding a column width for formatters whose output has no fixed maximum, and a drained footer printing a seven-run result beneath a single-invocation table. `render` now writes the banner and calls `render_observation(out, &measure())`. The banner stays outside because it is a fresh topology read rather than a function of the observation -- which is what keeps the rendering half pure and so drivable by a fixture. The binary moved to `src/bin/queue_contention/main.rs` to carry a sibling `tests.rs`, following `windows-placement-probe`'s layout; git recorded it as a rename, so history follows. **The cases are data.** `corpus.json` holds an observation and what the report must be true of, so adding one needs no Rust. The central check is derived rather than restated: every line of a named table must be the same length, which is exactly the property a cell wider than its column breaks. That catches width bugs the corpus never anticipated, where a golden catches only what somebody thought to record and needs regenerating whenever prose moves. Two things the corpus established on its first run, both of which a hand-written fixture would have missed: - With an empty observation the `ns/op range` table emits a header and no rows, while the layout and scaling tables still emit six `--` rows -- those iterate `PRODUCER_COUNTS`, that one iterates the runs. The first expectation written was wrong about this, not the renderer. - **A width of 22 was not overrun by an ordinary outlier.** The 300ms-against-4ns repetition that motivated the original finding renders 21 characters; reaching 23 needs a hundredfold ratio as well. The case was corrected after claiming otherwise, and its `why` now states the real argument: no constant can be established as sufficient, because the cell's width is a function of measured data. Sabotage-verified: putting the constant widths back fails the corpus case, naming the table and the overrun line. The first attempt at that sabotage appeared to pass, which was a stale binary -- cargo could not finalize its incremental directory -- and not the check failing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 27 +-- .../COMPLETED-CHECKLIST.md | 39 ++++ crates/windows-platform-probes/Cargo.toml | 2 +- .../src/bin/queue_contention/corpus.json | 147 ++++++++++++++ .../main.rs} | 28 ++- .../src/bin/queue_contention/tests.rs | 183 ++++++++++++++++++ 6 files changed, 396 insertions(+), 30 deletions(-) create mode 100644 crates/windows-platform-probes/src/bin/queue_contention/corpus.json rename crates/windows-platform-probes/src/bin/{queue_contention.rs => queue_contention/main.rs} (93%) create mode 100644 crates/windows-platform-probes/src/bin/queue_contention/tests.rs diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 1f9e18a21..4968ed61f 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -191,32 +191,7 @@ correctness in the archive. explicitly after being pointed at the question. Nothing in the suite decides it either way, which is itself the argument for making it an invariant rather than a test. -- [ ] **M4.7** -- Make the queue-contention report renderer testable, by taking the observation as an - argument instead of measuring inside it. - - **Gap:** `render` in [src/bin/queue_contention.rs](src/bin/queue_contention.rs) calls `measure()` - itself, so the only way to exercise it is to run the whole ~65-second host-dependent measurement. - Everything it does beyond the library's `render_table` is therefore unreached by the suite: the - three tables' assembly, the derived column widths, the `cfg`-dependent `Wide` rows, and the - interpretation text between them. - - **This is not hypothetical, and that is the argument for the item.** Two defects shipped through - exactly this gap on the branch that wrote it. The scaling and drained-comparison tables hard-coded - a column width of 22 for formatters whose output has no fixed maximum, so a wide cell silently - pushed later columns out of line with their headers; and the drained footer printed a seven-run - control result beneath a table produced by a single invocation. Both were found by reading. A - fixture over the renderer would have caught the first mechanically and made the second visible. - - **Target:** a sibling that takes `&Observation` -- `render` keeps its signature and calls it with - `measure()`, so the binary's behaviour is unchanged -- and fixture-based tests over synthetic - observations. [src/topology_report.rs](src/topology_report.rs) is the precedent in this crate: - rendering moved into the library precisely so fixture observations could drive it. Note the test - file cannot be an inline `mod tests` per the repository's Rust rules, and a binary needs the - `src/bin//main.rs` layout to carry a sibling `tests.rs`; - `windows-placement-probe`'s `src/bin/placement_probe/` is the worked example. - - Reported by review against this branch, and queued rather than taken because it is a refactor of - the report's structure at a point where the branch is converging, not a defect. +- [x] **M4.7** -- Make the queue-contention report renderer testable, by taking the observation as an argument instead of measuring inside it. -> [completed 2026-09-16 UTC-04:00](COMPLETED-CHECKLIST.md#m47) - [ ] **M2.15** -- Run the probe suite on a second architecture in CI. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index f38b0f879..ae6e36bc4 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1237,3 +1237,42 @@ branch, it sits in an append-only archive dated by its own heading, and there th of the record -- the entry exists to say that the estimate was wrong and that re-measuring decided the question. Rewriting it would have been an archive rewrite in service of tidiness. The design note's new wording agrees with it rather than contradicting it. + +## Moved 2026-09-16 18:40:00 UTC-04:00 -- M4.7: the report renderer, driven by a corpus + +### M4.7 -- Make the queue-contention report renderer testable, by taking the observation as an argument instead of measuring inside it. *(completed 2026-09-16 18:40:00 UTC-04:00)* + +`render` called `measure()` itself, so the only way to exercise it was a ~65-second host-dependent +pass. Everything beyond the library's `render_table` was therefore reached by nothing in the suite: +the three tables' assembly, the derived column widths, the `cfg`-gated `Wide` rows, and the prose +between them. Two defects shipped through that gap on this branch -- two tables hard-coding a column +width for formatters whose output has no fixed maximum, and a drained footer printing a seven-run +result beneath a table produced by one invocation. + +`render` now writes the banner and calls `render_observation(out, &measure())`. The banner stays +outside because it is a fresh topology read rather than a function of the observation, which is what +keeps the rendering half pure and therefore drivable by a fixture. The binary moved to +`src/bin/queue_contention/main.rs` so it can carry a sibling `tests.rs`, following +`windows-placement-probe`'s layout; git recorded it as a rename, so history follows. + +**The cases are data, not code.** `corpus.json` holds an observation and what the rendered report +must be true of, so adding a case needs no Rust. The central check is *derived rather than +restated*: `aligned_tables` asserts every line of a named table is the same length, which is exactly +the property a cell wider than its column breaks. It therefore catches width bugs the corpus never +anticipated, where a golden would only catch what somebody thought to record and would need +regenerating whenever the prose moved. + +Two things the corpus established on first run, both of which a hand-written fixture would have +missed: + +- With an empty observation the `ns/op range` table emits a header and no rows, while the layout and + scaling tables still emit six `--` rows, because those iterate `PRODUCER_COUNTS` and that one + iterates the runs. The first expectation written was wrong about this, not the renderer. +- **A width of 22 was not overrun by an ordinary outlier.** The 300ms-against-4ns repetition that + motivated the original finding renders 21 characters; reaching 23 needs a hundredfold ratio as + well. The argument for deriving the width is that no constant can be *established* as sufficient, + since the cell's width is a function of measured data -- not that 22 was visibly too small. The + corpus case says so in its own `why`, having been corrected once for claiming otherwise. + +Sabotage-verified: replacing the derived widths with the constant they had before fails the corpus +case, naming the table and the overrun line. diff --git a/crates/windows-platform-probes/Cargo.toml b/crates/windows-platform-probes/Cargo.toml index 44ed1a7fe..cef8b8d87 100644 --- a/crates/windows-platform-probes/Cargo.toml +++ b/crates/windows-platform-probes/Cargo.toml @@ -84,7 +84,7 @@ path = "src/bin/request_cost.rs" [[bin]] name = "probe-queue-contention" -path = "src/bin/queue_contention.rs" +path = "src/bin/queue_contention/main.rs" # These two are the same code, and that is the measurement: they differ only in # whether `build.rs` embeds the `longPathAware` manifest, which is not a runtime diff --git a/crates/windows-platform-probes/src/bin/queue_contention/corpus.json b/crates/windows-platform-probes/src/bin/queue_contention/corpus.json new file mode 100644 index 000000000..1d05d1001 --- /dev/null +++ b/crates/windows-platform-probes/src/bin/queue_contention/corpus.json @@ -0,0 +1,147 @@ +{ + "_comment": [ + "Cases that drive the queue-contention report renderer. Each supplies an", + "observation and what the rendered report must be true of. Adding a case is", + "adding data -- no Rust changes -- which is the point: the report's tables", + "were unreachable by the suite until M4.7, and two defects shipped through", + "that gap.", + "", + "`aligned_tables` names a header substring. Every non-blank line of that", + "table, header included, must be the same length. That is exactly the", + "property a cell wider than its column breaks, and it is derived from the", + "output rather than restated as a golden -- so it catches a width bug the", + "corpus never anticipated, which a golden cannot." + ], + "cases": [ + { + "name": "ordinary", + "why": "A plausible run. Establishes that the tables render and line up at all, so the stressed cases below are testing something.", + "observation": { + "available_parallelism": 8, + "isolated": [ + ["baseline_fetch_add", 1, 2.3, 434782608.0, 0, 2.1, 2.5], + ["baseline_fetch_add", 2, 12.1, 165289256.0, 0, 11.0, 13.0], + ["slotwise_mpsc", 1, 6.3, 158730158.0, 0, 6.0, 6.8], + ["slotwise_mpsc", 2, 50.6, 39525691.0, 0, 19.3, 59.5], + ["reserving_mpsc", 1, 5.4, 185185185.0, 0, 5.2, 5.9], + ["reserving_mpsc", 2, 31.9, 31347962.0, 0, 22.5, 35.2], + ["permit_mpsc", 1, 7.9, 126582278.0, 0, 7.5, 8.3], + ["permit_mpsc", 2, 44.2, 22624434.0, 0, 37.4, 45.9], + ["reserving(32/32)", 1, 5.5, 181818181.0, 0, 5.3, 6.0], + ["reserving(32/32)", 2, 32.4, 30864197.0, 0, 23.0, 35.9], + ["reserving(16/48)", 1, 5.6, 178571428.0, 0, 5.4, 6.1], + ["reserving(16/48)", 2, 33.1, 30211480.0, 0, 23.4, 36.2], + ["reserving(8/56)", 1, 5.7, 175438596.0, 0, 5.5, 6.2], + ["reserving(8/56)", 2, 33.4, 29940119.0, 0, 23.8, 36.5] + ], + "drained": [ + ["slotwise_mpsc", 1, 11.4, 87719298.0, 0, 10.9, 12.1], + ["slotwise_mpsc", 2, 66.8, 29940119.0, 1200, 60.1, 70.2], + ["reserving_mpsc", 1, 26.2, 38167938.0, 0, 24.9, 27.8], + ["reserving_mpsc", 2, 64.2, 31152647.0, 1100, 58.3, 68.0], + ["permit_mpsc", 1, 64.3, 15552099.0, 0, 61.0, 67.1], + ["permit_mpsc", 2, 56.2, 35587188.0, 900, 51.4, 59.9], + ["reserving(32/32)", 1, 24.8, 40322580.0, 0, 23.6, 26.3], + ["reserving(32/32)", 2, 65.1, 30721966.0, 1150, 59.0, 69.1], + ["reserving(16/48)", 1, 26.8, 37313432.0, 0, 25.4, 28.2], + ["reserving(16/48)", 2, 67.9, 29455081.0, 1180, 61.2, 71.4], + ["reserving(8/56)", 1, 27.6, 36231884.0, 0, 26.1, 29.0], + ["reserving(8/56)", 2, 67.0, 29850746.0, 1160, 60.5, 70.8] + ] + }, + "expect": { + "aligned_tables": ["16/48 vs", "reserving/slotwise", "atomic floor", "ns/op range"], + "contains": ["processors available to this process: 8"], + "absent": [] + } + }, + { + "name": "spans_wide_enough_to_overrun_a_fixed_column", + "why": "A cell's width is a function of measured data: `format_ratio_bounded` sizes its interval from the observed span, so no constant is provably sufficient. These values are chosen to exceed a fixed width rather than to depict a likely run -- and the margin matters, because the width of 22 that two tables carried was NOT overrun by an ordinary outlier. A 300ms repetition against a 4ns one renders 21 characters. Reaching 23 needs a hundredfold ratio as well. The argument for deriving the width is that the bound cannot be established, not that 22 was obviously too small.", + "observation": { + "available_parallelism": 8, + "isolated": [ + ["baseline_fetch_add", 1, 2.3, 434782608.0, 0, 2.1, 2.5], + ["baseline_fetch_add", 2, 12.1, 165289256.0, 0, 11.0, 13.0], + ["slotwise_mpsc", 1, 6.0, 166666666.0, 0, 4.0, 6.0], + ["slotwise_mpsc", 2, 30.0, 66666666.0, 0, 4.0, 60.0], + ["reserving_mpsc", 1, 5.0, 200000000.0, 0, 4.0, 6.0], + ["reserving_mpsc", 2, 3000.0, 666666.0, 0, 40.0, 90000.0], + ["permit_mpsc", 1, 8.0, 125000000.0, 0, 4.0, 6.0], + ["permit_mpsc", 2, 3000.0, 666666.0, 0, 40.0, 90000.0], + ["reserving(32/32)", 1, 5.0, 200000000.0, 0, 4.0, 6.0], + ["reserving(32/32)", 2, 30.0, 66666666.0, 0, 4.0, 6.0], + ["reserving(16/48)", 1, 50.0, 20000000.0, 0, 40.0, 6000.0], + ["reserving(16/48)", 2, 3000.0, 666666.0, 0, 40.0, 90000.0], + ["reserving(8/56)", 1, 60.0, 16666666.0, 0, 40.0, 7000.0], + ["reserving(8/56)", 2, 3200.0, 625000.0, 0, 40.0, 95000.0] + ], + "drained": [ + ["slotwise_mpsc", 1, 11.0, 90909090.0, 0, 4.0, 6.0], + ["slotwise_mpsc", 2, 30.0, 66666666.0, 1200, 4.0, 60.0], + ["reserving_mpsc", 1, 26.0, 38461538.0, 0, 4.0, 60.0], + ["reserving_mpsc", 2, 3000.0, 666666.0, 1100, 40.0, 90000.0], + ["permit_mpsc", 1, 64.0, 15625000.0, 0, 4.0, 60.0], + ["permit_mpsc", 2, 3000.0, 666666.0, 900, 40.0, 90000.0], + ["reserving(32/32)", 1, 25.0, 40000000.0, 0, 4.0, 6.0], + ["reserving(32/32)", 2, 30.0, 66666666.0, 1150, 4.0, 6.0], + ["reserving(16/48)", 1, 250.0, 4000000.0, 0, 40.0, 9000.0], + ["reserving(16/48)", 2, 3000.0, 666666.0, 1180, 40.0, 90000.0], + ["reserving(8/56)", 1, 270.0, 3703703.0, 0, 40.0, 9000.0], + ["reserving(8/56)", 2, 3200.0, 625000.0, 1160, 40.0, 95000.0] + ] + }, + "expect": { + "aligned_tables": ["16/48 vs", "reserving/slotwise", "atomic floor", "ns/op range"], + "contains": ["100.00x [6.67-22500.00]"], + "absent": [] + } + }, + { + "name": "a_shape_that_did_not_run", + "why": "A row present but carrying the did-not-run sentinel. Zero is not an obviously broken value in any of this report's columns, so the rendered cells must be marked rather than published: `0.0` ns/op reads as immeasurably fast and `0.00x` as a ratio of one.", + "observation": { + "available_parallelism": 4, + "isolated": [ + ["baseline_fetch_add", 1, 2.3, 434782608.0, 0, 2.1, 2.5], + ["slotwise_mpsc", 1, 6.3, 158730158.0, 0, 6.0, 6.8], + ["reserving_mpsc", 1, 5.4, 185185185.0, 0, 5.2, 5.9], + ["permit_mpsc", 1, 0.0, 0.0, 0, 0.0, 0.0], + ["reserving(32/32)", 1, 5.5, 181818181.0, 0, 5.3, 6.0], + ["reserving(16/48)", 1, 0.0, 0.0, 0, 0.0, 0.0], + ["reserving(8/56)", 1, 5.7, 175438596.0, 0, 5.5, 6.2] + ], + "drained": [ + ["slotwise_mpsc", 1, 11.4, 87719298.0, 0, 10.9, 12.1], + ["reserving_mpsc", 1, 26.2, 38167938.0, 0, 24.9, 27.8], + ["permit_mpsc", 1, 0.0, 0.0, 0, 0.0, 0.0], + ["reserving(32/32)", 1, 24.8, 40322580.0, 0, 23.6, 26.3], + ["reserving(16/48)", 1, 0.0, 0.0, 0, 0.0, 0.0], + ["reserving(8/56)", 1, 27.6, 36231884.0, 0, 26.1, 29.0] + ] + }, + "expect": { + "aligned_tables": ["16/48 vs", "reserving/slotwise", "atomic floor", "ns/op range"], + "contains": ["--"], + "absent": ["0.00x", " 0.0 ", "infx", "NaN"] + } + }, + { + "name": "nothing_measured_at_all", + "why": "Every shape absent, which is what a `cfg`-elided target or a failed run looks like from the renderer's side. It must still produce a report with its headers rather than panicking or printing figures it does not have.", + "observation": { + "available_parallelism": null, + "isolated": [], + "drained": [] + }, + "expect": { + "aligned_tables": ["16/48 vs", "reserving/slotwise", "atomic floor"], + "contains": [ + "processors available to this process: unknown (the query failed)", + "ns/op range" + ], + "absent": ["0.00x", "infx", "NaN"] + } + } + ] +} diff --git a/crates/windows-platform-probes/src/bin/queue_contention.rs b/crates/windows-platform-probes/src/bin/queue_contention/main.rs similarity index 93% rename from crates/windows-platform-probes/src/bin/queue_contention.rs rename to crates/windows-platform-probes/src/bin/queue_contention/main.rs index f02b347f3..418a94d7e 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/main.rs @@ -14,7 +14,7 @@ //! cannot separate. use windows_platform_probes::queue_contention::{ - DRAINED_CAPACITY, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, format_nanos, + DRAINED_CAPACITY, Observation, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, format_nanos, format_ratio_bounded, format_scaling_bounded, measure, ratio_column_width, render_table, shapes, }; @@ -27,8 +27,26 @@ fn main() { emit_report(render); } -/// The probe's whole report, as text. +#[cfg(test)] +mod tests; + +/// Measure, then render what was measured. +/// +/// **The two halves are separate so the second one can be tested.** Rendering +/// used to measure inside itself, which made the only way to exercise it a +/// ~65-second host-dependent run -- so the table assembly, the derived column +/// widths, the `cfg`-gated rows and the prose between them were reached by +/// nothing in the suite. Two defects shipped through that gap on this branch: +/// two tables hard-coded a column width for formatters whose output has no +/// fixed maximum, and the drained footer printed a seven-run result beneath a +/// table produced by one invocation. See `M4.7`. fn render(out: &mut dyn std::fmt::Write) { + // The banner is the one line that is not a function of the observation -- + // it is a fresh topology read -- so it is written here and + // `render_observation` stays a pure function of what was measured. That + // purity is the whole point of the split: it is what lets a fixture drive + // the entire report. + // // First line of the report, and part of the returned text rather than // written out here: a captured report must carry the line naming the // machine that produced it, and the taint marker with it. @@ -37,12 +55,16 @@ fn render(out: &mut dyn std::fmt::Write) { "{}", windows_placement_probe::fingerprint::banner_line() ); + render_observation(out, &measure()); +} + +/// Everything the report says about an observation. +fn render_observation(out: &mut dyn std::fmt::Write, observation: &Observation) { let _ = writeln!( out, "== how does the array queue's push path scale with producer count? ==\n" ); - let observation = measure(); // `available_parallelism`, not the host count -- an affinity mask or job // object narrows it, and saying "host reports" under either would contradict // the banner three lines up. The host's shape is already there; this is what diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs new file mode 100644 index 000000000..01d8428f7 --- /dev/null +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -0,0 +1,183 @@ +// Copyright (c) Mike Grier. + +//! The report renderer, driven by a corpus rather than by hand-written cases. +//! +//! Adding a case is adding data to `corpus.json` -- an observation, and what the +//! rendered report must be true of. Nothing here needs to change, which is the +//! point: the report's tables were reachable by nothing in the suite until +//! `M4.7`, because rendering measured inside itself and the only way to run it +//! was a ~65-second host-dependent pass. +//! +//! **The central check is derived, not restated.** `aligned_tables` asserts that +//! every line of a named table is the same length. That is exactly the property +//! a cell wider than its column breaks, and it holds for inputs the corpus never +//! anticipated -- where a golden would only catch what somebody thought to +//! record, and would have to be regenerated every time the prose moved. + +use super::render_observation; +use serde_json::Value; +use windows_platform_probes::queue_contention::{Observation, Run}; + +/// Compiled in, so a missing or malformed corpus is a build failure rather than +/// a test that silently runs nothing. +const CORPUS: &str = include_str!("corpus.json"); + +/// A row is `[shape, producers, nanos_per_op, ops_per_second, refusals, +/// fastest, slowest]`, positionally. +/// +/// The struct literal is exhaustive, so adding a field to [`Run`] stops this +/// file compiling -- which is the reminder to decide what the corpus should say +/// about it, rather than letting a new field go unexercised. +fn run_from(value: &Value) -> Run { + let row = value.as_array().expect("a run is an array"); + let number = |index: usize| -> f64 { + row[index] + .as_f64() + .unwrap_or_else(|| panic!("field {index} of a run is a number")) + }; + Run { + // Leaked so the fixture can hand back the `&'static str` the field + // wants. A test process is the one place that is the cheap answer, and + // the corpus is a fixed compiled-in set, so this cannot grow. + shape: Box::leak( + row[0] + .as_str() + .expect("a shape is a string") + .to_owned() + .into_boxed_str(), + ), + producers: number(1) as usize, + nanos_per_op: number(2), + ops_per_second: number(3), + refusals: number(4) as u64, + fastest_nanos_per_op: number(5), + slowest_nanos_per_op: number(6), + } +} + +fn observation_from(value: &Value) -> Observation { + let rows = |key: &str| -> Vec { + value[key] + .as_array() + .unwrap_or_else(|| panic!("`{key}` is an array")) + .iter() + .map(run_from) + .collect() + }; + Observation { + isolated: rows("isolated"), + drained: rows("drained"), + available_parallelism: value["available_parallelism"] + .as_u64() + .map(|count| count as usize), + } +} + +/// The lines of the table whose header contains `header`, header included. +/// +/// A table runs from its header to the first blank line. The drained tables +/// carry a second header row, which is part of the table and has to line up +/// with the rest of it, so it is not skipped. +fn table_lines<'a>(report: &'a str, header: &str) -> Vec<&'a str> { + let all: Vec<&str> = report.lines().collect(); + let start = all + .iter() + .position(|line| line.contains(header)) + .unwrap_or_else(|| panic!("no table header containing {header:?} in:\n{report}")); + all[start..] + .iter() + .take_while(|line| !line.trim().is_empty()) + .copied() + .collect() +} + +#[test] +fn every_corpus_case_renders_a_report_whose_tables_line_up() { + let corpus: Value = serde_json::from_str(CORPUS).expect("the corpus parses"); + let cases = corpus["cases"].as_array().expect("`cases` is an array"); + assert!( + !cases.is_empty(), + "an empty corpus would pass every assertion below without testing anything" + ); + + for case in cases { + let name = case["name"].as_str().expect("a case is named"); + let why = case["why"].as_str().expect("a case says why it exists"); + let observation = observation_from(&case["observation"]); + + let mut report = String::new(); + render_observation(&mut report, &observation); + + let expect = &case["expect"]; + for header in expect["aligned_tables"] + .as_array() + .expect("`aligned_tables` is an array") + { + let header = header.as_str().expect("a header is a string"); + let lines = table_lines(&report, header); + assert!( + lines.len() > 1, + "[{name}] the table at {header:?} has no rows, so its alignment \ + is not being checked\n{why}" + ); + let width = lines[0].len(); + for line in &lines { + assert_eq!( + line.len(), + width, + "[{name}] a cell overran its column, so every column after it \ + no longer lines up with its header.\n{why}\n\ + header ({width}): {:?}\n line ({}): {line:?}", + lines[0], + line.len() + ); + } + } + + for needle in expect["contains"] + .as_array() + .expect("`contains` is an array") + { + let needle = needle.as_str().expect("a needle is a string"); + assert!( + report.contains(needle), + "[{name}] expected {needle:?} in the report.\n{why}\n{report}" + ); + } + + for needle in expect["absent"].as_array().expect("`absent` is an array") { + let needle = needle.as_str().expect("a needle is a string"); + assert!( + !report.contains(needle), + "[{name}] {needle:?} must not reach the report.\n{why}\n{report}" + ); + } + } +} + +/// The alignment check must be able to fail, or the corpus proves nothing. +/// +/// Every case above passes, which is indistinguishable from a check that cannot +/// fail. This hands the detector a deliberately overrun table and asserts it +/// notices -- the anti-vacuity half of a sabotage run, made in-suite because the +/// detector is the instrument here. +#[test] +fn the_alignment_check_can_tell_a_misaligned_table_from_an_aligned_one() { + let misaligned = "producers ratio\n1 1.00x [1.00-1.00]\n"; + let lines = table_lines(misaligned, "ratio"); + assert_eq!(lines.len(), 2, "the fixture has a header and one row"); + assert_ne!( + lines[0].len(), + lines[1].len(), + "this fixture exists to be misaligned; if it is not, the corpus check is \ + being asked to detect something that is not there" + ); + + let aligned = "producers ratio\n 1 1.00x\n"; + let lines = table_lines(aligned, "ratio"); + assert_eq!( + lines[0].len(), + lines[1].len(), + "and it must not call an aligned table misaligned" + ); +} From ab0014ba5b0357699d7100c32af8ca139dbf6bbc Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 19:02:38 -0400 Subject: [PATCH 087/139] test(probes): close the three rendering gaps cargo-mutants found Ran `cargo mutants` over the two rendering modules, as the renderer had just become reachable by the suite for the first time. **The renderer itself:** 3 mutants, and the split landed where it was aimed. `render_observation` -- the 480 lines of table assembly, derived widths, `cfg`-gated rows and interpretation -- is killed by the corpus. The two survivors are `main` and `render`, which exist only to call `measure()` and so cannot be reached without the ~65-second run. The mutable logic moved behind a testable boundary and the untestable remainder is now trivial. **The library module:** 205 mutants, 128 caught, 8 unviable, 0 timeouts, 69 missed -- and the shape of the 69 is the finding. Sixty-six are in the `time_*` measurement functions, which the suite cannot reach for the same reason: running them means running the probe. Those are a structural property of a benchmark, not a gap a test can close cheaply. That left three survivors in code that *is* reachable, and all three were real: - `Observation::scaling_bounds` -- `||` could become `&&` in the guard that rejects an unmeasured row, because every case written supplied the same row twice. One real row against one that never ran is exactly what the guard is for, and nothing reached it. - `format_scaling_bounded` -- the `low.is_finite() && high.is_finite()` guard could be replaced with `true`, or its `&&` with `||`. Every case gave the bound a finite pair or no pair at all, so the guard was never asked to reject one. The point estimate is still publishable in that case; only the interval is not, which the new test pins in both directions. Re-running mutants restricted to those two functions: 24 of 24 caught, against 21 of 24 before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/queue_contention/tests.rs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 1ca42b839..96cba17de 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -1295,6 +1295,77 @@ fn scaling_bounds_at_one_producer_is_exactly_one() { } } +/// Either row being unmeasured is enough; it does not take both. +/// +/// Found by `cargo mutants`: replacing the `||` in that guard with `&&` survived +/// the suite, because every case written supplied the same row twice. A bound +/// over one real row and one that never ran is exactly the case the guard is +/// for, and nothing reached it. +#[test] +fn scaling_bounds_needs_both_rows_measured_not_merely_one() { + let measured = run_spanning(shapes::RESERVING_MPSC, 1, 4.0, 5.0, 6.0); + let mut absent = run_spanning(shapes::RESERVING_MPSC, 8, 40.0, 50.0, 60.0); + absent.nanos_per_op = 0.0; + + let one_ran = Observation { + isolated: vec![measured, absent], + drained: Vec::new(), + available_parallelism: Some(8), + }; + assert_eq!( + one_ran.scaling_bounds(&one_ran.isolated, shapes::RESERVING_MPSC, 8), + None, + "the many-producer row measured nothing, so there is no bound" + ); + + let mut absent_one = measured; + absent_one.nanos_per_op = 0.0; + let other_ran = Observation { + isolated: vec![ + absent_one, + run_spanning(shapes::RESERVING_MPSC, 8, 40.0, 50.0, 60.0), + ], + drained: Vec::new(), + available_parallelism: Some(8), + }; + assert_eq!( + other_ran.scaling_bounds(&other_ran.isolated, shapes::RESERVING_MPSC, 8), + None, + "and the one-producer row measuring nothing is equally disqualifying" + ); +} + +/// A non-finite bound is not printed, even beside a perfectly good point. +/// +/// Found by `cargo mutants`: the `low.is_finite() && high.is_finite()` guard +/// could be replaced with `true`, or its `&&` with `||`, and the suite stayed +/// green -- every case gave the bound a finite pair or no pair at all, so the +/// guard was never asked to reject one. The point estimate is still +/// publishable in that case; only the interval is not. +#[test] +fn format_scaling_bounded_drops_a_non_finite_interval_and_keeps_the_point() { + for (low, high) in [ + (f64::NAN, 2.0), + (1.5, f64::NAN), + (f64::NEG_INFINITY, 2.0), + (1.5, f64::INFINITY), + ] { + let rendered = format_scaling_bounded(Some(2.0), Some((low, high))); + assert_eq!( + rendered, "2.00x", + "[{low}, {high}] is not an interval, so only the point may be \ + published -- got {rendered:?}" + ); + } + + // And a finite pair must still be printed, or the guard has simply been + // turned into "never show an interval". + assert_eq!( + format_scaling_bounded(Some(2.0), Some((1.5, 2.5))), + "2.00x [1.50-2.50]" + ); +} + /// The stop flag must be set on the path where nobody sets it explicitly. /// /// The drained timers cleared the flag on the line after their producer scope, From 24de194085e55b0591f116ce0638865fc76b36d7 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 19:39:17 -0400 Subject: [PATCH 088/139] refactor(probes): drop the producer handle in all four drained timers `time_drained_layout` released its stop guard and joined the consumer without the explicit `drop(tx)` its three siblings perform between those two lines. **Verified immaterial before changing it**, since the value of a consistency fix is nil if the inconsistency was load-bearing in one direction: the consumer exits on the `done` flag that `drop(stop)` sets, not on sender disconnection; its `pop` is non-blocking and treats empty and disconnected alike; and both `elapsed` and `refusals` are settled before either drop. The handle was released at end of scope regardless. So this changes no behaviour. It is here because four sibling functions differing in one line is a question every later reader has to answer from scratch, and because the explicit release states the teardown order -- the producers are gone, now join the consumer -- rather than leaving it to the end of the function. Raised by review as an instance of the "fix applied one call site short" class and correctly declined as a defect. Taking it anyway: the cost is one line, and the alternative is an asymmetry that looks like it might mean something. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/src/queue_contention.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 5d55c4324..d4ed39921 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -1542,6 +1542,7 @@ fn time_drained_layout(producers: usize) -> Repetition }); let elapsed = measured_span(&spans); drop(stop); + drop(tx); let refusals = consumer.join().expect("the consumer must not panic"); (elapsed, refusals) } From 43445a9eea31f6f8e14f0c8e80ed73871f2f7386 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 20:05:39 -0400 Subject: [PATCH 089/139] docs(probes): refuse to certify a capture the summariser could not read Reported by review, and the mechanism is exact. A report renders `--` for a shape that did not run, `Number("--")` is `NaN`, and every comparison against `NaN` is false -- so a single unreadable cell emptied the "outside the band" list and the script printed every layout median inside the control band: true with the words `span: NaNx to NaNx` two lines above it. Demonstrated rather than argued: running the previous script against a capture with one blanked comparison cell prints exactly that, and exits 0. Now every parsed cell is checked for finiteness, a layout row without a matching comparison row is reported, and a run missing a producer count is reported. If anything is unreadable the script names each problem, prints `UNKNOWN` instead of a verdict, and exits non-zero. The same input now yields: CAPTURE INCOMPLETE -- not summarised: - run1.txt, drained comparison, 2 producers: the reserving_mpsc cost: "--" is not a number - run1.txt: 2 producers has a layout row but no comparison row Output on the committed capture is byte-identical to `summary.txt`, so the published figures are unchanged. This is the second defect of the same shape in this file, after it was found checking only the largest median. Both are the branch's oldest class -- a failure sentinel rendering as a plausible result -- landing in the artifact whose stated purpose is that the derivation can be checked rather than trusted. Nothing executes that artifact in CI, which is why sabotage is the only thing that finds these. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/summarise.js | 84 +++++++++++++++++-- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 967bdff65..397764cb6 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -18,8 +18,27 @@ function median(values) { : sorted[middle]; } +// Problems are collected rather than thrown, so one malformed capture reports +// everything wrong with it instead of only the first thing. +// +// **Nothing here may quietly degrade to `NaN`.** Every comparison against `NaN` +// is false, so a `NaN` bound makes the containment filter below find nothing +// outside it and print `true` -- certifying a capture it could not read. A +// report renders `--` for a shape that did not run, and `Number("--")` is +// exactly that `NaN`. +const problems = []; + +function finite(text, what) { + const value = Number(text); + if (!Number.isFinite(value)) { + problems.push(`${what}: ${JSON.stringify(text)} is not a number`); + return null; + } + return value; +} + // producers -> { narrowNanos, ratios: [16/48, 8/56, 64/64] } -function drainedLayout(lines) { +function drainedLayout(lines, path) { let start = -1; lines.forEach((line, i) => { if (line.includes("-- drained --")) start = i; @@ -27,19 +46,39 @@ function drainedLayout(lines) { const rows = new Map(); for (const line of lines.slice(start + 2, start + 8)) { const fields = line.trim().split(/\s+/); + const producers = finite(fields[0], `${path}: a drained layout producer count`); + if (producers === null) continue; + const where = `${path}, drained layout, ${producers} producers`; + const narrowNanos = finite(fields[1], `${where}: the 32/32 cost`); const ratios = [...line.matchAll(RATIO)].map((m) => Number(m[1])); - rows.set(Number(fields[0]), { narrowNanos: Number(fields[1]), ratios }); + // A row that did not run renders `--`, which the ratio pattern does not + // match, so a short list is the signal that this row cannot be summarised. + if (ratios.length !== 3) { + problems.push( + `${where}: found ${ratios.length} layout ratios, expected 3`, + ); + continue; + } + if (narrowNanos === null) continue; + rows.set(producers, { narrowNanos, ratios }); } return rows; } // producers -> reserving ns/op, from the comparison table -function drainedComparison(lines) { +function drainedComparison(lines, path) { const start = lines.findIndex((line) => line.includes("reserving/slotwise")); const rows = new Map(); for (const line of lines.slice(start + 2, start + 8)) { const fields = line.trim().split(/\s+/); - rows.set(Number(fields[0]), Number(fields[2])); + const producers = finite(fields[0], `${path}: a comparison producer count`); + if (producers === null) continue; + const reserving = finite( + fields[2], + `${path}, drained comparison, ${producers} producers: the reserving_mpsc cost`, + ); + if (reserving === null) continue; + rows.set(producers, reserving); } return rows; } @@ -49,15 +88,22 @@ const layouts = []; const controls = []; for (const path of paths) { const lines = fs.readFileSync(path, "utf8").split(/\r?\n/); - const layout = drainedLayout(lines); - const comparison = drainedComparison(lines); + const layout = drainedLayout(lines, path); + const comparison = drainedComparison(lines, path); layouts.push(layout); // The same code measured twice in one run: `reserving_mpsc` in the comparison // table against `32/32` in the layout table. const control = new Map(); for (const [producers, row] of layout) { + const reserving = comparison.get(producers); + if (reserving === undefined) { + problems.push( + `${path}: ${producers} producers has a layout row but no comparison row`, + ); + continue; + } if (row.narrowNanos > 0) { - control.set(producers, comparison.get(producers) / row.narrowNanos); + control.set(producers, reserving / row.narrowNanos); } } controls.push(control); @@ -70,8 +116,13 @@ console.log("drained layout ratios vs 32/32, median across runs"); console.log("producers 16/48 8/56 64/64"); const medians = []; for (const p of producers) { + const present = layouts.filter((layout) => layout.has(p)); + if (present.length !== layouts.length) { + problems.push(`${p} producers is missing from ${layouts.length - present.length} run(s)`); + continue; + } const row = [0, 1, 2].map((column) => - median(layouts.map((layout) => layout.get(p).ratios[column])), + median(present.map((layout) => layout.get(p).ratios[column])), ); medians.push(...row); console.log( @@ -83,6 +134,23 @@ const every = controls.flatMap((control) => [...control.values()]); console.log(""); console.log("same-code control (reserving_mpsc vs reserving 32/32), drained"); console.log(` observations: ${every.length}`); + +// **Refuse to certify a capture that could not be read.** Everything below +// compares against the control band, and every comparison against `NaN` is +// false -- so a single unreadable cell would empty the "outside the band" list +// and print `true`. An incomplete capture must say so, not pass. +if (problems.length > 0 || every.length === 0 || medians.length === 0) { + console.log(""); + console.log("CAPTURE INCOMPLETE -- not summarised:"); + if (every.length === 0) console.log(" - no control observations were read"); + if (medians.length === 0) console.log(" - no layout medians were read"); + for (const problem of problems) console.log(` - ${problem}`); + console.log(""); + console.log("every layout median inside the control band: UNKNOWN"); + process.exitCode = 1; + return; +} + console.log( ` span: ${Math.min(...every).toFixed(2)}x to ${Math.max(...every).toFixed(2)}x`, ); From 625b41045a9cf38859e86c1a985793691044a465 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 20:10:40 -0400 Subject: [PATCH 090/139] docs(probes): report the drained figures per count, and withdraw the verdict The capture ended with `every layout median inside the control band: true`. That is a verdict, published in an artifact whose stated purpose is that the derivation can be checked rather than taken on trust, and cited from the design notes as evidence that the finding stood. `D-no-client-prescriptions` asks for what was observed, under what parameters, and what the procedure could not determine -- and to stop there. **Publishing the verdict is what let it be wrong.** The control was pooled across producer counts into one band, and the control is not independent of producer count: about 0.82-0.98x at one producer against 0.95-1.23x at thirty-two in this capture. A pooled band is therefore wider than any single count's, and containment followed from the method rather than from the data. Compared per count, several medians fall outside their own count's range -- notably 64/64 at one producer, 1.17x against 0.82-0.98x. Comparing per count does not rescue a verdict either: three runs give three control observations per count, and the range of three samples is not a band. A fresh draw falls outside the range of three priors about half the time by construction. So three runs do not settle the drained comparison in either direction. The script now prints the per-count control range beside that count's layout medians and emits no true/false at all; had it done that from the start, the 64/64 row would have been visible to any reader and there would have been nothing to withdraw, because nothing would have been asserted. Corrected in all three homes: the capture README, the design-note amendment, and the archived `M4.3` entry that repeated it. **The caveat reaches further than this capture, and the note now says so.** The pre-handshake reading compares a per-count median against a control quoted as one pooled figure -- "the widest median is 1.13x at one producer, against a control that reaches 1.27x" -- which is the same comparison being withdrawn here. Its raw runs were never committed, so the per-count bands cannot be recomputed and that conclusion is neither confirmed nor refuted; settling it needs the sweep re-run with its data kept. Reported by review, after the pooled framing had already been published. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../COMPLETED-CHECKLIST.md | 14 ++- .../windows-platform-probes/DESIGN-NOTES.md | 44 ++++++-- .../2026-09-16-drained-handshake/README.md | 20 +++- .../2026-09-16-drained-handshake/summarise.js | 104 ++++++++++-------- .../2026-09-16-drained-handshake/summary.txt | 26 ++--- 5 files changed, 135 insertions(+), 73 deletions(-) diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index ae6e36bc4..e6e491983 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1199,9 +1199,17 @@ derivation can be checked rather than trusted. **The figures are amended rather than replaced**, because this is new data and not a correction: the earlier capture remains what the earlier instrument measured, and both are labelled with the code -that produced them. The finding survived the re-measurement -- every layout median still sits inside -the same-code control band in the drained regime -- which is worth stating precisely because it was -not guaranteed: the drained conclusion did not depend on the window it had been measured through. +that produced them. + +**What the re-measurement establishes was overstated when this entry was written, and the correction +belongs here.** It originally said the finding survived -- every layout median still inside the +same-code control band. That rested on pooling every control observation into one band, and the +pooling produced the answer: the control is not independent of producer count, so a pooled band is +wider than any count's own and containment follows from the method. Compared per count, several +medians fall outside their own count's range; compared per count the other way, three runs give +three control observations, which is not a band. Three runs do not settle the drained comparison in +either direction. The capture now reports per-count figures and emits no verdict, and the +pre-handshake reading rests on the seven-run sweep, which this does not replace. ## Moved 2026-09-16 17:31:31 UTC-04:00 -- M2.16: the census that broke the prose around it diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 5f897f4fe..8ea2ebacf 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1055,16 +1055,40 @@ In the drained regime nothing separates at all -- every u64 layout *and* the 128-bit word sit inside the control band at every producer count (the widest median is 1.13x at one producer, against a control that reaches 1.27x). -**Amended 2026-09-16: re-measured after the `M4.3` handshake, and the finding -stands.** That paragraph was taken before producers held for the consumer, so it -describes a drained regime whose opening was briefly undrained. Re-measured on -the same host without that window, every layout median still sits inside the -same-code control band -- the figures, and the check itself, are in -[captures/2026-09-16-drained-handshake/](captures/2026-09-16-drained-handshake/README.md) -rather than retyped here. What the re-measurement establishes is narrow and -worth stating plainly: the drained conclusion did not depend on the window it -was measured through. It says nothing about the isolated regime, which the -handshake does not touch. +**Amended 2026-09-16: re-measured after the `M4.3` handshake, and the +re-measurement does not settle it.** That paragraph was taken before producers +held for the consumer, so it describes a drained regime whose opening was +briefly undrained. Re-measured on the same host without that window, the figures +are in +[captures/2026-09-16-drained-handshake/](captures/2026-09-16-drained-handshake/README.md). + +**What the re-measurement establishes is less than first claimed here, and the +correction is worth stating plainly.** This amendment originally said every +layout median still sat inside the same-code control band. That rested on +pooling every control observation into one band, and the pooling is what +produced the answer: the control is not independent of producer count -- it +spans about 0.82-0.98x at one producer against 0.95-1.23x at thirty-two in that +capture -- so pooling builds a band wider than any count's own, and containment +follows from the method. Compared per count, several medians fall outside their +own count's range. Compared per count the other way, three runs give three +control observations, and the range of three samples is not a band to judge +anything against. + +So the drained comparison is **not established by three runs**, in either +direction. The capture reports the per-count figures and declines a verdict; +the pre-handshake reading above rests on the seven-run sweep, which this does +not replace. Reported by review, after the pooled framing had already been +published here. + +**The same caveat reaches the paragraph above, and saying so is the honest +scope of this correction.** That reading compares a per-count median against a +control quoted as a single pooled figure -- "the widest median is 1.13x at one +producer, against a control that reaches 1.27x" -- which is the same comparison +this amendment has just withdrawn for the three-run capture. Its raw runs are +not committed, so the per-count bands behind it cannot be recomputed here and +the conclusion is neither confirmed nor refuted. What can be said is that it +rests on the same framing, and that settling it needs the sweep re-run with its +data kept. **Widening the word is the one effect this probe establishes.** At sixteen and thirty-two producers the isolated 128-bit rows fall outside the same-code diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md index c61632b40..5e861361e 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md @@ -39,8 +39,22 @@ node summarise.js run1.txt run2.txt run3.txt ``` It is committed so the derivation can be checked rather than taken on trust, and -so nothing downstream has to retype a figure. The script derives two things the -runs do not state individually: the across-run median per producer count, and -the same-code control span -- which is a relation *between* two tables, since +so nothing downstream has to retype a figure. The script derives what the runs do +not state individually: the across-run median per producer count, and the +same-code control -- which is a relation *between* two tables, since `reserving_mpsc` in the comparison table and `32/32` in the layout table are the same configuration measured twice in the same run. + +**It reports per producer count and emits no verdict, deliberately.** An earlier +version pooled every control observation into one band and asked whether each +layout median fell inside it. It answered `true`, and the pooling is what +produced that answer: the control is not independent of producer count -- about +0.82-0.98x at one producer against 0.95-1.23x at thirty-two here -- so a pooled +band is wider than any count's own, and containment follows from the method +rather than from the data. Comparing per count does not rescue a verdict either, +because three runs give three control observations per count, and the range of +three samples is not a band to judge anything against. + +So three runs do not settle the drained comparison in either direction. This +capture reports figures; the claim that nothing separates in the drained regime +rests on the seven-run sweep, which this does not replace. diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 397764cb6..c9a05294b 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -111,65 +111,83 @@ for (const path of paths) { const producers = [...layouts[0].keys()].sort((a, b) => a - b); console.log(`runs: ${paths.length}`); -console.log(""); -console.log("drained layout ratios vs 32/32, median across runs"); -console.log("producers 16/48 8/56 64/64"); -const medians = []; + +// **Reported per producer count, and deliberately without a verdict.** +// +// An earlier version pooled every control observation into one band and asked +// whether each layout median fell inside it. That answered `true`, and the +// answer was an artifact of the pooling: the control is not independent of +// producer count -- it spans about 0.82-0.98x at one producer against +// 0.95-1.23x at thirty-two on this capture -- so pooling builds a band wider +// than any count's own and containment follows from the method rather than +// from the data. +// +// Comparing per count instead does not rescue a verdict either. Three runs +// give three control observations per count, and the range of three samples is +// not a band: a fresh independent draw falls outside the range of three priors +// about half the time. So neither comparison is strong enough to say a median +// is inside or outside, and this script says so rather than picking whichever +// framing yields an answer. +const rows = []; for (const p of producers) { const present = layouts.filter((layout) => layout.has(p)); if (present.length !== layouts.length) { - problems.push(`${p} producers is missing from ${layouts.length - present.length} run(s)`); + problems.push( + `${p} producers is missing from ${layouts.length - present.length} run(s)`, + ); continue; } - const row = [0, 1, 2].map((column) => - median(present.map((layout) => layout.get(p).ratios[column])), - ); - medians.push(...row); - console.log( - `${String(p).padStart(9)} ` + row.map((m) => `${m.toFixed(2)}x`).join(" "), - ); + const control = controls.map((c) => c.get(p)).filter((v) => Number.isFinite(v)); + if (control.length !== controls.length) { + problems.push(`${p} producers has no control ratio in every run`); + continue; + } + rows.push({ + producers: p, + control, + medians: [0, 1, 2].map((column) => + median(present.map((layout) => layout.get(p).ratios[column])), + ), + }); } -const every = controls.flatMap((control) => [...control.values()]); -console.log(""); -console.log("same-code control (reserving_mpsc vs reserving 32/32), drained"); -console.log(` observations: ${every.length}`); - -// **Refuse to certify a capture that could not be read.** Everything below -// compares against the control band, and every comparison against `NaN` is -// false -- so a single unreadable cell would empty the "outside the band" list -// and print `true`. An incomplete capture must say so, not pass. -if (problems.length > 0 || every.length === 0 || medians.length === 0) { +// **Refuse to summarise a capture that could not be read.** A report renders +// `--` for a shape that did not run, `Number("--")` is `NaN`, and every +// comparison against `NaN` is false -- so an unreadable cell used to empty the +// "outside the band" list and print `true`. +if (problems.length > 0 || rows.length === 0) { console.log(""); console.log("CAPTURE INCOMPLETE -- not summarised:"); - if (every.length === 0) console.log(" - no control observations were read"); - if (medians.length === 0) console.log(" - no layout medians were read"); + if (rows.length === 0) console.log(" - no complete producer counts were read"); for (const problem of problems) console.log(` - ${problem}`); - console.log(""); - console.log("every layout median inside the control band: UNKNOWN"); process.exitCode = 1; return; } +console.log(""); console.log( - ` span: ${Math.min(...every).toFixed(2)}x to ${Math.max(...every).toFixed(2)}x`, + "drained, per producer count: the same-code control's observed range, then", ); -console.log(` median: ${median(every).toFixed(2)}x`); +console.log("each layout's median ratio against 32/32, across runs."); +console.log(""); +console.log("producers control(n) 16/48 8/56 64/64"); +for (const row of rows) { + const low = Math.min(...row.control); + const high = Math.max(...row.control); + const band = `${low.toFixed(2)}-${high.toFixed(2)}(${row.control.length})`; + console.log( + `${String(row.producers).padStart(9)} ${band.padEnd(16)} ` + + row.medians.map((m) => `${m.toFixed(2)}x`).join(" "), + ); +} + +const everyControl = rows.flatMap((row) => row.control); console.log(""); -const low = Math.min(...every); -const high = Math.max(...every); -// **Every** layout median, not just the largest. The claim this capture is -// cited for is that all of them sit inside the control band, and an earlier -// version of this check tested `Math.max(...medians)` alone -- which passes -// unchanged while a median below the band's floor goes unreported. The -// committed data happens to clear the floor, so that check was right by luck -// rather than by construction. -const outside = medians.filter((m) => m < low || m > high); console.log( - `layout medians: ${medians.length}, spanning ` + - `${Math.min(...medians).toFixed(2)}x to ${Math.max(...medians).toFixed(2)}x`, + `control observations: ${everyControl.length} across ${rows.length} producer counts, ` + + `${Math.min(...everyControl).toFixed(2)}x to ${Math.max(...everyControl).toFixed(2)}x pooled`, ); -console.log(`every layout median inside the control band: ${outside.length === 0}`); -if (outside.length > 0) { - console.log(` outside: ${outside.map((m) => `${m.toFixed(2)}x`).join(", ")}`); -} +console.log( + "Pooled only to show the spread; it is not a band to judge a median against,", +); +console.log("for the reason recorded in this script beside the table above."); diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt index 6d0badfc1..2e16335be 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summary.txt @@ -1,18 +1,16 @@ runs: 3 -drained layout ratios vs 32/32, median across runs -producers 16/48 8/56 64/64 - 1 1.01x 1.03x 1.17x - 2 0.98x 1.03x 1.06x - 4 1.00x 1.00x 1.09x - 8 0.98x 1.01x 1.06x - 16 0.91x 0.93x 0.80x - 32 1.23x 1.20x 0.94x +drained, per producer count: the same-code control's observed range, then +each layout's median ratio against 32/32, across runs. -same-code control (reserving_mpsc vs reserving 32/32), drained - observations: 18 - span: 0.75x to 1.23x - median: 0.98x +producers control(n) 16/48 8/56 64/64 + 1 0.82-0.98(3) 1.01x 1.03x 1.17x + 2 0.92-1.06(3) 0.98x 1.03x 1.06x + 4 0.95-1.02(3) 1.00x 1.00x 1.09x + 8 0.92-1.00(3) 0.98x 1.01x 1.06x + 16 0.75-1.04(3) 0.91x 0.93x 0.80x + 32 0.95-1.23(3) 1.23x 1.20x 0.94x -layout medians: 18, spanning 0.80x to 1.23x -every layout median inside the control band: true +control observations: 18 across 6 producer counts, 0.75x to 1.23x pooled +Pooled only to show the spread; it is not a band to judge a median against, +for the reason recorded in this script beside the table above. From 56ff3f779ac0e55437a31e4dac3dd722b882eca7 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 20:19:59 -0400 Subject: [PATCH 091/139] docs: state what rustdoc's include can and cannot do The rule justified "a number belongs in an artifact" by asserting that "rustdoc has no data include". That is false, and this repository is the counterexample: `windows-waitable-queues`' lib.rs splices its entire README into the crate docs with `#[doc = include_str!("../README.md")]`. The constraint the rule was reaching for is real but narrower. The include is whole-file or nothing -- neither markdown nor rustdoc can pull a single measured value out of a data file and into the middle of a sentence -- so a figure quoted mid-paragraph has to be typed there, which is what makes pasting the path of least resistance. Stated that way the rule rests on something true, and a reader who knows about `include_str!` is no longer given a reason to distrust it. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f723ef757..1acc39d36 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1273,7 +1273,11 @@ Two corollaries that have each already cost a review round: ### 4. Prose carries the claim; a number belongs in an artifact Measured data pasted into prose becomes a copy somebody must keep true by hand, in every place it -was pasted, forever. Markdown has no include and rustdoc has no data include, so pasting is the path +was pasted, forever. Markdown has no include at all; rustdoc has one, and this repository uses it -- +`windows-waitable-queues`' [lib.rs](../crates/windows-waitable-queues/src/lib.rs) splices its whole +README in with `#[doc = include_str!("../README.md")]`. What neither has is a way to pull a *single +measured value* out of a data file and into a sentence: the include is whole-file or nothing, so a +figure quoted mid-paragraph must be typed there. That is why pasting is the path of least resistance — and it is where this repository's documentation defects overwhelmingly come from. Measured on one pull request's review history: almost none of its measurement findings were *wrong measurements*; they were transcriptions that drifted — a table disagreeing with its own copy From b2012a1d9d5b409b74ba6c628fc5898428de9d2c Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 20:19:59 -0400 Subject: [PATCH 092/139] docs(queue): correct the capacity ceiling per layout, and pin it to the source Two published claims were wrong, both by being stated more broadly than they hold. **The capacity ceiling is the layout's, not the shape's.** "On a 64-bit target `slotwise_mpsc` reaches 2^62 slots and `reserving_mpsc` 2^31" was written when `Balanced` was the only layout, and `BOUNDS_MAX` is one bit narrower than whatever position the layout carries. Measured on the constants: `Balanced` 2^31, `Enduring` 2^47, `Perpetual` 2^55, `Wide` 2^62. So the flat figure understates three of the four layouts, and under `Wide` the two shapes reach the same number -- the crate-wide ceiling -- which is a better reason for the bullet's point than the one it gave. **The wrap is cumulative, not per run.** The bullet asked whether you push more than ~4 billion items "in one run", while the recurrence section it links to correctly says the position advances over the queue's whole life. Many short bursts reach the wrap as surely as one long one, so the narrower question let a reader who never has a long run conclude the hazard was not theirs. **The four numbers are now pinned.** A reader is entitled to rely on them and nothing could fail if they drifted, so they are asserted against `BOUNDS_MAX` in a const block: widening or narrowing a layout's position stops the build. Held to a 64-bit target, since on a 32-bit one the crate-wide 2^30 binds first and every layout lands on it. Verified in both directions -- the assertions compile as written, and changing the `Wide` expectation to 2^61 fails the build with E0080, so they are load-bearing rather than decorative. Reported by review; the `Wide` half was reported, the `Enduring` and `Perpetual` half was found by checking the class rather than the instance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/README.md | 19 ++++++++++++------- .../src/reserving_mpsc.rs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 55d1beb6a..7b3b5267f 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -375,8 +375,10 @@ both rather than picking one for you. **What distinguishes them:** -- **Pushing more than ~4 billion items in one run, from two or more producers?** +- **Pushing more than ~4 billion items through one queue, from two or more producers?** Under its default layout `reserving_mpsc` can lose an item past that volume. + The count is cumulative over that queue's whole life, not per run: many short + bursts reach the wrap as surely as one long one. `slotwise_mpsc`'s positions are 64 bits under every configuration, and naming a deeper layout on `reserving_mpsc` moves the recurrence out -- `Perpetual` to about twenty years -- though what that costs in throughput is not established. @@ -480,12 +482,15 @@ moved an SPSC handoff by 5.6x on an earlier host this workspace measured. The Two things that look like reasons to choose and are not: -- **Capacity.** On a 64-bit target `slotwise_mpsc` reaches 2^62 slots and - `reserving_mpsc` 2^31. On a 32-bit one the crate-wide ceiling is 2^30 and - **both** shapes land there -- `reserving_mpsc`'s packed 2^31 is clamped down - to it as well -- so the difference disappears and the comparison means - nothing. Either way it counts slots allocated up front, not items ever pushed: - a ring of 2^31 slots is tens of gigabytes before it holds anything useful. +- **Capacity.** `reserving_mpsc` has no single ceiling: it is the layout's, one + bit narrower than that layout's position. On a 64-bit target `Balanced` + reaches 2^31, `Enduring` 2^47, `Perpetual` 2^55, and `Wide` 2^62 -- the last + being the crate-wide ceiling, which is also `slotwise_mpsc`'s, so under `Wide` + the two shapes reach the same number and there is nothing to compare. On a + 32-bit target the crate-wide ceiling is 2^30 and every layout of both shapes + lands there, so the difference disappears again. Either way it counts slots + allocated up front, not items ever pushed: a ring of 2^31 slots is tens of + gigabytes before it holds anything useful. - **`slotwise_mpsc` winning at one producer.** True in one regime, and at one producer you want `spsc` anyway. diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 036417cf0..3f3490d37 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -657,6 +657,24 @@ impl ClaimLayout for Wide { const POSITION_BITS: u32 = 64; } +/// The per-layout capacity ceilings the README publishes, pinned to the source. +/// +/// [The README](../README.md) names 2^31, 2^47, 2^55 and 2^62 for the four +/// layouts on a 64-bit target, and the reader is entitled to rely on them. +/// Nothing in a markdown file can fail a build, so the numbers are asserted +/// against `BOUNDS_MAX` here: widening or narrowing a layout's position moves +/// one of these and stops the build, which is the prompt to go and correct the +/// prose. Held to a 64-bit target because on a 32-bit one the crate-wide +/// ceiling binds first and every layout lands on it instead. +#[cfg(target_pointer_width = "64")] +const _: () = { + assert!(::BOUNDS_MAX == 1usize << 31); + assert!(::BOUNDS_MAX == 1usize << 47); + assert!(::BOUNDS_MAX == 1usize << 55); + #[cfg(feature = "dwcas")] + assert!(::BOUNDS_MAX == 1usize << 62); +}; + /// The position after `position`, wrapping at the width the layout gives it. /// /// **Centralised because the width is no longer the type's.** A position is From 96f5059bec935eed86068f19aacda27627e443c2 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 20:19:59 -0400 Subject: [PATCH 093/139] docs(probes): describe the drained consumer as looping on pop, not continuously `await_consumer` says it exactly: "It does *not* guarantee the consumer is draining continuously from then on -- nothing a flag can express would, since the consumer can be descheduled at any point afterwards." Five other places went on calling the regime "a consumer popping continuously" or "a continuously draining consumer", including the banner the probe prints above its own drained tables -- so the report asserted the thing the code two screens away denies. This is the sixth time on this branch a correction has landed on the site a reviewer named and stopped there, so this one swept the proposition rather than the phrasing: the module doc, the `drained` field, both `time_drained_*` helpers, the report banner, and the design note's regime paragraph. All six now say the consumer loops on `pop`, which describes what the thread runs without claiming how continuously it is scheduled to run it. The `drained` field's doc carries the guarantee in full, since that is the type a reader reaches first. The committed capture keeps the old banner text, because that is what its instrument printed. Its README now says so and says why the runs are not retaken: the label moved, nothing measured did. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/DESIGN-NOTES.md | 4 ++-- .../captures/2026-09-16-drained-handshake/README.md | 6 ++++++ .../src/bin/queue_contention/main.rs | 2 +- .../windows-platform-probes/src/queue_contention.rs | 13 +++++++++---- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 8ea2ebacf..b1606813d 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -743,8 +743,8 @@ Two regimes, and the pair is the point. whatever curve appears against N is the producer side alone, with no consumer traffic in it. It is not the claim alone -- what is timed is each shape's whole push path, tail claim and slot write and publication and doorbell together, so a difference here is a difference in PUSH COST rather than -evidence about the claim on its own. **Drained** runs a consumer popping -continuously, which is the regime in which `reserving_mpsc`'s read of `head` is most expensive -- that +evidence about the claim on its own. **Drained** runs a consumer looping on `pop`, +which is the regime in which `reserving_mpsc`'s read of `head` is most expensive -- that read is cheap until a consumer is *writing* the line, and measuring it in isolation would report it as free. It neither isolates that read nor bounds it: the ratio is between two complete push paths whose other diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md index 5e861361e..5118cc79e 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md @@ -29,6 +29,12 @@ The host is the same machine as the capture the crate README carries, so the two are comparable; nothing here says anything about any other hardware, and the banner's `numa[16]` is a single node holding all sixteen processors. +The runs' regime banner reads `a consumer popping continuously`, which is what +`68198359` printed. A later commit changed that label to `a consumer looping on +pop`, because the handshake guarantees the consumer's pop path has run once, not +that it is scheduled without gaps. The label is the only difference: the runs +below are not retaken for it, since nothing about what was measured moved. + ## Reading it [summary.txt](summary.txt) is the output of [summarise.js](summarise.js) over the diff --git a/crates/windows-platform-probes/src/bin/queue_contention/main.rs b/crates/windows-platform-probes/src/bin/queue_contention/main.rs index 418a94d7e..10ec08078 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/main.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/main.rs @@ -129,7 +129,7 @@ fn render_observation(out: &mut dyn std::fmt::Write, observation: &Observation) let _ = writeln!( out, - "\n-- drained: a consumer popping continuously, capacity {DRAINED_CAPACITY} --" + "\n-- drained: a consumer looping on pop, capacity {DRAINED_CAPACITY} --" ); render_table(out, &observation.drained); diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index d4ed39921..f9566f614 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -49,7 +49,7 @@ //! difference in PUSH COST, and attributing it to the claim alone would be //! reading more out of the number than is in it. Found by a review. //! -//! - **Drained** -- a consumer popping continuously while the producers push. +//! - **Drained** -- a consumer looping on `pop` while the producers push. //! This is the regime in which `reserving_mpsc`'s read of `head` is at its //! most expensive, because `head` is only costly to read when a consumer is //! *writing* it. Measured in isolation that read hits a clean, shared line and @@ -260,7 +260,12 @@ impl Run { pub struct Observation { /// Producers timed with no consumer and no possibility of refusal. pub isolated: Vec, - /// Producers timed against a continuously draining consumer. + /// Producers timed against a consumer looping on `pop`. + /// + /// The handshake in [`await_consumer`] guarantees that loop has executed at + /// least once before any producer starts timing. It does not guarantee the + /// consumer is never descheduled afterwards, so "looping" describes what the + /// consumer thread runs, not how continuously it is scheduled to run it. pub drained: Vec, /// Processors available to **this process**, when it could be determined. /// @@ -1322,7 +1327,7 @@ fn time_drained_reserving(producers: usize) -> Repetition { (elapsed, refusals) } -/// The experimental permit claim, against a continuously draining consumer. +/// The experimental permit claim, against a consumer looping on `pop`. /// /// The regime that can price the claim honestly, for the same reason the /// reserving twin needs it: the shared line a producer touches is only @@ -1464,7 +1469,7 @@ fn time_isolated_layout(producers: usize) -> Repetition { (elapsed, refusals) } -/// One claim-word layout, against a continuously draining consumer. +/// One claim-word layout, against a consumer looping on `pop`. /// /// Generic for [`time_isolated_layout`]'s reason. fn time_drained_layout(producers: usize) -> Repetition { From 0782577717109832d8fc27037117e4cc5f109bbd Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 20:19:59 -0400 Subject: [PATCH 094/139] test(probes): route the anti-vacuity check through the assertion the corpus calls The test is named "the alignment check can tell a misaligned table from an aligned one", and it did not call the alignment check. It compared two fixture strings' lengths itself, so it was a second copy of the rule rather than an exercise of it -- and deleting the corpus loop's `assert_eq!` outright would have left it passing. A test that cannot notice its subject's removal is exactly the vacuity it was written to rule out. The rule now has one statement, `assert_aligned`, which the corpus check calls and the test drives through `catch_unwind`: the misaligned fixture must make it panic, the aligned one must not. Verified by sabotage -- gutting `assert_aligned`'s body turns the test red, naming the 28-wide row it accepted against a 20-wide header, where before the change the same sabotage passed. CONTRACT INTEGRITY rule 1, applied to a rule I had restated in my own test while the same review round was correcting me for restating rules elsewhere. The caught panic prints through the default hook, so a stray message appears in the test's output on success. Left alone deliberately: suppressing it means a process-global no-op panic hook, and this suite runs its tests as threads in one process, so that window would swallow a concurrent test's failure message. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention/tests.rs | 85 ++++++++++++------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs index 01d8428f7..2d2019838 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -16,8 +16,32 @@ use super::render_observation; use serde_json::Value; +use std::panic::AssertUnwindSafe; use windows_platform_probes::queue_contention::{Observation, Run}; +/// Asserts every line of a table shares its header's width. +/// +/// **The one statement of the alignment rule.** The corpus check calls it, and +/// so does the anti-vacuity test at the bottom of this file, so that test +/// exercises the assertion the corpus actually relies on. A test that compared +/// the line lengths itself would be a second copy of the rule, and would keep +/// passing after this body was deleted. +#[track_caller] +fn assert_aligned(name: &str, lines: &[&str], why: &str) { + let width = lines[0].len(); + for line in lines { + assert_eq!( + line.len(), + width, + "[{name}] a cell overran its column, so every column after it \ + no longer lines up with its header.\n{why}\n\ + header ({width}): {:?}\n line ({}): {line:?}", + lines[0], + line.len() + ); + } +} + /// Compiled in, so a missing or malformed corpus is a build failure rather than /// a test that silently runs nothing. const CORPUS: &str = include_str!("corpus.json"); @@ -120,18 +144,7 @@ fn every_corpus_case_renders_a_report_whose_tables_line_up() { "[{name}] the table at {header:?} has no rows, so its alignment \ is not being checked\n{why}" ); - let width = lines[0].len(); - for line in &lines { - assert_eq!( - line.len(), - width, - "[{name}] a cell overran its column, so every column after it \ - no longer lines up with its header.\n{why}\n\ - header ({width}): {:?}\n line ({}): {line:?}", - lines[0], - line.len() - ); - } + assert_aligned(name, &lines, why); } for needle in expect["contains"] @@ -158,26 +171,38 @@ fn every_corpus_case_renders_a_report_whose_tables_line_up() { /// The alignment check must be able to fail, or the corpus proves nothing. /// /// Every case above passes, which is indistinguishable from a check that cannot -/// fail. This hands the detector a deliberately overrun table and asserts it -/// notices -- the anti-vacuity half of a sabotage run, made in-suite because the -/// detector is the instrument here. +/// fail. This hands [`assert_aligned`] a deliberately overrun table and asserts +/// it panics -- the anti-vacuity half of a sabotage run, made in-suite because +/// the detector is the instrument here. +/// +/// It calls the same function the corpus calls rather than re-deriving the rule +/// from the fixture's line lengths. A test that compared the lengths itself +/// would keep passing after [`assert_aligned`]'s body was deleted, which is +/// exactly the failure it exists to rule out. +/// +/// The caught panic prints its message through the default hook, so a backtrace +/// line appears in this test's output on success. That is left alone: silencing +/// it means installing a process-global no-op panic hook, and this suite runs +/// its tests as threads in one process, so the window would swallow a concurrent +/// test's failure message. #[test] fn the_alignment_check_can_tell_a_misaligned_table_from_an_aligned_one() { - let misaligned = "producers ratio\n1 1.00x [1.00-1.00]\n"; - let lines = table_lines(misaligned, "ratio"); - assert_eq!(lines.len(), 2, "the fixture has a header and one row"); - assert_ne!( - lines[0].len(), - lines[1].len(), - "this fixture exists to be misaligned; if it is not, the corpus check is \ - being asked to detect something that is not there" + let misaligned = table_lines( + "producers ratio\n1 1.00x [1.00-1.00]\n", + "ratio", ); - - let aligned = "producers ratio\n 1 1.00x\n"; - let lines = table_lines(aligned, "ratio"); - assert_eq!( - lines[0].len(), - lines[1].len(), - "and it must not call an aligned table misaligned" + assert_eq!(misaligned.len(), 2, "the fixture has a header and one row"); + let caught = std::panic::catch_unwind(AssertUnwindSafe(|| { + assert_aligned("fixture", &misaligned, "a deliberately overrun table"); + })); + assert!( + caught.is_err(), + "the detector passed a table whose row is {} wide against a {} header; \ + it cannot report a real overrun either", + misaligned[1].len(), + misaligned[0].len() ); + + let aligned = table_lines("producers ratio\n 1 1.00x\n", "ratio"); + assert_aligned("fixture", &aligned, "an aligned table must not be reported"); } From 4e1e886e12fe7c1be1d040cdd48b150ca5c57729 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 20:46:46 -0400 Subject: [PATCH 095/139] docs(probes): derive the isolated regime in the capture, and cross-check the table The capture's `summarise.js` derives the drained tables only. The isolated figures are cited too -- by the queue crate's documentation, which tells a reader what `Wide` costs on the push path -- so they were being taken on trust from a seven-run sweep whose raw runs were never committed. `isolated.js` derives them from the same three runs, against the same control the drained script uses: `reserving_mpsc` and `reserving(32/32)` are the same code under two names, so the gap between them is what "no difference" looks like on this host. It reports per producer count and, like its sibling, emits no verdict; the last section says plainly that three control observations per count is not a band. The two measurements agree, which is the useful part -- the seven-run sweep now has an independent second reading on the isolated side even though its own raw data is gone: | producers | seven-run | three-run capture | control | |---|---|---|---| | 1 | 1.37x | 1.39x | 1.04x [0.95-1.26] | | 8 | 1.82x | 1.67x | 0.99x [0.90-1.01] | | 32 | 3.81x | 4.77x | 0.98x [0.93-1.06] | In the capture all three runs put 64/64 above the control's whole observed range at every producer count; 16/48 does so at 16 and 32, and 8/56 at 32 -- which is what D-41 already says about the deeper u64 layouts, now with a runnable derivation behind it. Recorded in the design note that owns the isolated table, beside the table rather than in an analysis document, since it changes what that table can be relied on for. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 32 ++++++ .../2026-09-16-drained-handshake/README.md | 17 ++- .../2026-09-16-drained-handshake/isolated.js | 108 ++++++++++++++++++ .../2026-09-16-drained-handshake/isolated.txt | 20 ++++ 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js create mode 100644 crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index b1606813d..876670ead 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1055,6 +1055,38 @@ In the drained regime nothing separates at all -- every u64 layout *and* the 128-bit word sit inside the control band at every producer count (the widest median is 1.13x at one producer, against a control that reaches 1.27x). +**Amended 2026-09-16: the isolated table above has a committed cross-check, and +it agrees.** The seven-run sweep's raw runs were not kept, which is the gap the +drained amendment below is about. The three-run capture taken for `M4.3` records +the isolated regime as well, and +[isolated.js](captures/2026-09-16-drained-handshake/isolated.js) derives it -- +so the isolated figures, unlike the drained ones, can be checked against a +second independent measurement. The two agree on 64/64 at every producer count: + +| producers | seven-run (above) | three-run capture | same-code control, capture | +|---|---|---|---| +| 1 | 1.37x [1.16-1.57] | 1.39x [1.32-1.74] | 1.04x [0.95-1.26] | +| 2 | 1.13x [1.02-1.15] | 1.10x [1.10-1.16] | 1.03x [1.00-1.04] | +| 4 | 1.29x [1.14-1.36] | 1.37x [1.36-1.70] | 0.96x [0.95-1.03] | +| 8 | 1.82x [1.64-2.20] | 1.67x [1.59-2.05] | 0.99x [0.90-1.01] | +| 16 | 3.45x [2.91-4.27] | 4.00x [3.94-4.10] | 0.92x [0.88-1.16] | +| 32 | 3.81x [2.70-4.31] | 4.77x [3.54-4.99] | 0.98x [0.93-1.06] | + +In the capture, all three runs put 64/64 above the control's whole observed +range at **every** producer count; 16/48 does so at 16 and 32, and 8/56 at 32. +Three control observations per count is not a band, so this reports what these +runs did rather than what a fresh run would do. + +**An earlier reading of the small-count end said "near parity at one or two", +and that is withdrawn.** It was restated in six places across the queue crate -- +rustdoc, the crate doc, the README twice, `Cargo.toml`, and D-41 -- while the +table directly above it read 1.37x at one producer, and the capture reads 1.39x +against a control of 1.04x. Whatever 1.37x is, it is not parity, and the phrase +asserted an absence of difference that neither measurement shows. The six sites +now say the path was measured as slower at every producer count measured, +smallest at one or two. Found by review of the isolated figures against the +capture. + **Amended 2026-09-16: re-measured after the `M4.3` handshake, and the re-measurement does not settle it.** That paragraph was taken before producers held for the consumer, so it describes a drained regime whose opening was diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md index 5118cc79e..4918d7902 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md @@ -44,8 +44,21 @@ three runs, regenerated with: node summarise.js run1.txt run2.txt run3.txt ``` -It is committed so the derivation can be checked rather than taken on trust, and -so nothing downstream has to retype a figure. The script derives what the runs do +[isolated.txt](isolated.txt) is the output of [isolated.js](isolated.js) over the +same three runs, regenerated with: + +``` +node isolated.js run1.txt run2.txt run3.txt +``` + +The two cover different regimes and are kept apart for that reason: `summarise.js` +derives the **drained** tables, `isolated.js` the **isolated** ones. The isolated +figures are cited by the queue crate's own documentation -- which says the whole +push path was measured as slower under `Wide` at every producer count -- so they +need a derivation a reader can run rather than a number taken on trust. + +Both are committed so the derivation can be checked rather than taken on trust, and +so nothing downstream has to retype a figure. The scripts derive what the runs do not state individually: the across-run median per producer count, and the same-code control -- which is a relation *between* two tables, since `reserving_mpsc` in the comparison table and `32/32` in the layout table are the diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js new file mode 100644 index 000000000..926177251 --- /dev/null +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -0,0 +1,108 @@ +// Isolated regime: each claim-word layout against the default, read against a +// same-code control. +// +// `summarise.js` beside this derives the DRAINED tables only. The isolated +// figures are cited too -- by the queue crate's docs, which say the whole push +// path was measured as slower under `Wide` at every producer count -- so they +// need a derivation a reader can run, not a number taken on trust. +// +// The control is the point. `reserving_mpsc` and `reserving(32/32)` are the SAME +// CODE under two names, so the gap between them is what "no difference" looks +// like on this host; a layout ratio only means something read against it. +// +// Three runs give three control observations per count. The range of three +// samples is NOT a band -- a fresh draw falls outside the range of three priors +// about half the time by construction -- so the last column reports what these +// runs did, and is not a claim about what the next run would do. +// +// Usage: node isolated.js run1.txt run2.txt run3.txt +"use strict"; + +const fs = require("fs"); + +const files = process.argv.slice(2); +if (files.length === 0) { + console.error("usage: node isolated.js [run.txt ...]"); + process.exit(2); +} + +const COUNTS = [1, 2, 4, 8, 16, 32]; +const DEFAULT_LAYOUT = "reserving(32/32)"; +// Same code as DEFAULT_LAYOUT, under the shipping type's own name. +const CONTROL_TWIN = "reserving_mpsc"; +const LAYOUTS = ["reserving(16/48)", "reserving(8/56)", "reserving(64/64)"]; + +// The isolated raw table only. The drained table repeats every shape name, so a +// whole-file scan would silently average the two regimes together. +function isolatedRows(file) { + const lines = fs.readFileSync(file, "utf8").split("\n"); + const start = lines.findIndex((l) => l.startsWith("-- isolated:")); + const end = lines.findIndex((l) => l.startsWith("-- drained:")); + if (start < 0 || end < 0 || end <= start) { + throw new Error(`${file}: expected an isolated marker followed by a drained one`); + } + const rows = new Map(); + for (const line of lines.slice(start, end)) { + const m = line.match(/^(\S+)\s+(\d+)\s+([\d.]+)\s/); + if (m) rows.set(`${m[1]}@${m[2]}`, Number(m[3])); + } + return rows; +} + +const tables = files.map(isolatedRows); + +function ratios(numerator, denominator) { + const out = new Map(); + for (const n of COUNTS) { + out.set( + n, + tables.map((t, i) => { + const a = t.get(`${numerator}@${n}`); + const b = t.get(`${denominator}@${n}`); + // A missing or unusable row is an error, not a skipped count: silently + // dropping one would quietly narrow every range printed below. + if (!Number.isFinite(a) || !Number.isFinite(b) || b <= 0) { + throw new Error(`${files[i]}: no usable ${numerator}/${denominator} at ${n} producers`); + } + return a / b; + }), + ); + } + return out; +} + +const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)]; +const fmt = (x) => x.toFixed(2); +const cell = (xs) => `${fmt(median(xs))}x [${fmt(Math.min(...xs))}-${fmt(Math.max(...xs))}]`; + +const control = ratios(DEFAULT_LAYOUT, CONTROL_TWIN); +const measured = new Map(LAYOUTS.map((l) => [l, ratios(l, DEFAULT_LAYOUT)])); + +console.log(`isolated regime, ${files.length} run(s): ${files.join(", ")}`); +console.log(`each layout against ${DEFAULT_LAYOUT}; control is ${DEFAULT_LAYOUT} against ${CONTROL_TWIN}`); +console.log("median of the per-run ratios, with the observed range beside it\n"); + +const head = ["producers".padEnd(11), "control".padEnd(20)].concat( + LAYOUTS.map((l) => l.replace("reserving", "").padEnd(20)), +); +console.log(head.join("")); +for (const n of COUNTS) { + const row = [String(n).padEnd(11), cell(control.get(n)).padEnd(20)]; + for (const l of LAYOUTS) row.push(cell(measured.get(l).get(n)).padEnd(20)); + console.log(row.join("")); +} + +console.log("\nwhere every run sat above the control's whole observed range:"); +for (const l of LAYOUTS) { + const above = COUNTS.filter((n) => { + const top = Math.max(...control.get(n)); + return measured.get(l).get(n).every((x) => x > top); + }); + console.log(` ${l.padEnd(18)} ${above.length ? above.join(", ") + " producers" : "no producer count"}`); +} + +console.log( + "\nThe control's range here is three observations per count, which is not a\n" + + "band. This reports what these runs did; it does not establish that a fresh\n" + + "run would land the same way.", +); diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt new file mode 100644 index 000000000..86d55bc47 --- /dev/null +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt @@ -0,0 +1,20 @@ +isolated regime, 3 run(s): run1.txt, run2.txt, run3.txt +each layout against reserving(32/32); control is reserving(32/32) against reserving_mpsc +median of the per-run ratios, with the observed range beside it + +producers control (16/48) (8/56) (64/64) +1 1.04x [0.95-1.26] 0.95x [0.94-1.00] 0.97x [0.95-1.00] 1.39x [1.32-1.74] +2 1.03x [1.00-1.04] 0.96x [0.93-0.97] 0.91x [0.90-0.95] 1.10x [1.10-1.16] +4 0.96x [0.95-1.03] 0.96x [0.86-1.02] 0.96x [0.93-0.99] 1.37x [1.36-1.70] +8 0.99x [0.90-1.01] 0.95x [0.95-1.11] 0.92x [0.88-1.14] 1.67x [1.59-2.05] +16 0.92x [0.88-1.16] 1.25x [1.18-1.29] 1.27x [1.14-1.29] 4.00x [3.94-4.10] +32 0.98x [0.93-1.06] 1.35x [1.34-1.42] 1.43x [1.26-1.45] 4.77x [3.54-4.99] + +where every run sat above the control's whole observed range: + reserving(16/48) 16, 32 producers + reserving(8/56) 32 producers + reserving(64/64) 1, 2, 4, 8, 16, 32 producers + +The control's range here is three observations per count, which is not a +band. This reports what these runs did; it does not establish that a fresh +run would land the same way. From 30df81500e327e41e486553c66f26fe7456b7c39 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 20:46:46 -0400 Subject: [PATCH 096/139] docs(queue): withdraw "near parity at one or two" for the Wide layout Six places said the whole push path under `Wide` was at "near parity at one or two" producers: the `Wide` rustdoc, the crate doc, the README twice, the `dwcas` comment in Cargo.toml, and D-41. The table those six point to reads **1.37x** at one producer, and the committed capture independently reads **1.39x [1.32-1.74]** against a same-code control of 1.04x [0.95-1.26]. Whatever 1.37x is, it is not parity -- and the phrase asserted an absence of difference that neither measurement shows. In the capture all three runs sit above the control's whole observed range at *every* producer count, one and two included, so the small-count end is where the margin is smallest rather than where it disappears. All six now say the path was measured as slower at every producer count measured, smallest at one or two, several times by thirty-two. No digits are pasted into the six: the per-count table stays the one place the figures are recorded, and it now carries the capture cross-check. **The drained half of the same sentence is corrected too.** The `Wide` rustdoc said the difference "fell inside that host's same-code control and could not be called at all", and D-41 said it fell inside the control when drained. That is the containment claim withdrawn in `625b4104`: it came from pooling the control across producer counts, and the control is not independent of producer count, so containment followed from the method rather than from the data. Both now say the drained comparison is not settled in either direction, and why. Verified: `cargo doc -p windows-waitable-queues --all-features` clean (CI denies broken intra-doc links), 345 lib tests, 12 doctests -- the README is included with `#[doc = include_str!]`, so its examples are compiled. Found by reviewing the isolated figures against the capture rather than from a reported site; a reviewer had raised the phrase as a judgement call and declined to call it, and computing the medians settled it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/Cargo.toml | 2 +- crates/windows-waitable-queues/DESIGN-NOTES.md | 2 +- crates/windows-waitable-queues/README.md | 8 ++++---- crates/windows-waitable-queues/src/lib.rs | 2 +- crates/windows-waitable-queues/src/reserving_mpsc.rs | 11 +++++++---- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/windows-waitable-queues/Cargo.toml b/crates/windows-waitable-queues/Cargo.toml index 903514f43..35c35fd2c 100644 --- a/crates/windows-waitable-queues/Cargo.toml +++ b/crates/windows-waitable-queues/Cargo.toml @@ -58,7 +58,7 @@ experimental-permit-claim = [] # target and the build fails naming it. # # This feature adds only the `Wide` layout. The whole push path was measured as -# slower under it as producer count rises -- near parity at one or two, several times by +# slower under it at every producer count measured -- smallest at one or two, several times by # thirty-two, in the isolated regime. The probe times the complete push, so that # is the layout's effect on that path, not the exchange in isolation. # diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index c11cf3979..cb9f7a9f3 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -75,7 +75,7 @@ preferred. | D-38 | **One atomic, one discipline: an atomic that carries any acquire/release operation has acquire/release on *every* operation, and a relaxed load is never mixed in.** A relaxed operation is still **atomic** -- indivisible, untorn, and free of data-race UB -- but it is *unordered*: it behaves like a plain load or store with respect to placement, unanchored relative to the ordered operations on the same object and free to be moved by the optimizer or the processor. It is not pinned to its textual site, so reasoning about it in statement order is nonsense. **The two axes are independent, and conflating them is the mistake in both directions:** relaxed does not mean "no guarantees" (see [D-40](#d-40) -- the atomicity is often the whole point), and it does not mean "ordered but weakly". The failure mode is that it usually does what the source appears to say, until a change of code generator or a weaker processor makes it not; on x86-64 TSO a decorative `Acquire` and a `Relaxed` load emit near-identical code, so a test suite on this host cannot see the difference at all -- which is the same blindness [D-31](#d-31) measured. **When the two resolutions differ, promote the load.** The exception is a reference count ([D-39](#d-39)), and it is an exception for a stated reason rather than by convention. | | D-39 | **The reference counts keep a relaxed increment against an `AcqRel` decrement, and that is the one sanctioned departure from [D-38](#d-38).** It is sanctioned because *no dependent memory is read on the strength of the relaxed increment*: a thread incrementing `producers` already holds a handle, so it needs no edge to learn the object exists, and the cache-coherence effect an acquire would buy is not required until the count reaches zero -- at which point the `AcqRel` decrement supplies it. That is the same argument `std::sync::Arc` makes, and it is a property of what the count is used for, not a general licence. A count whose value ever decided whether to *dereference* something would not qualify. | | D-40 | **A relaxed atomic is chosen for its *atomicity*, and dropping to a plain field is never the way to "simplify" one.** [D-38](#d-38) says a relaxed operation is unordered; it is still indivisible, and that is frequently the entire reason the field is an atomic at all. Without it the implementation may synthesise a wide access out of narrower ones and observe a **torn** value, and concurrent access to a non-atomic field is a data race and therefore UB regardless. `reserving_mpsc`'s claim word is the worked example: it packs `reserved` and `position` into one `u64` and is uniformly relaxed ([D-38](#d-38)), yet a torn read would yield a pair that never existed as a state and would break the compare-and-swap protocol outright. On `i686-pc-windows-msvc` -- which [D-18](#d-18) deliberately keeps supported -- that load must be a `cmpxchg8b` or an 8-byte SSE load, *more* expensive than the two `mov`s a plain `u64` would get, and the compiler is obliged to pay it for exactly this reason. **Relaxed is a statement about ordering only; it is never a step toward removing the atomic.** | -| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts sit outside the same-code control but too close to it to establish an ordering or a cost on this host: a flag to measure locally, not a finding. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). Which half the bits go to is the trade this decision exposes rather than settles: a 32-bit reservation field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, against a position whose width sets the recurrence horizon. The bound that matters on the reservation side is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. Which of the two a deployment needs is the deployment's question. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it as producer count rises in the isolated regime -- near parity at one or two, several times by thirty-two -- while falling inside the same-code control when drained. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a longer lifetime argument -- a recurrence at 2^64 rather than at 2^56 -- and not a different kind of argument. Like every horizon in that column it scales with the caller's push rate rather than being absolute. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | +| D-41 | **The claim word's apportionment is a caller's choice, and the recurrence behind SH-14.1 is a number the caller sets rather than one this crate imposes.** Supersedes [D-36](#d-36), whose premise was that the only fix was the [D-35](#d-35) claim-protocol replacement, gated on an open question -- so disclosing beat delaying. That premise was false, and measurement is what showed it: the 32/32 split followed from requiring the reservation half to hold the *entire capacity*, because every slot could be reserved at once. Capping outstanding reservations instead leaves the capacity bounded only by the ring, and the position is free to take 48 or 56 bits. `Balanced` (32/32), `Enduring` (16/48) and `Perpetual` (8/56) issue the **same** atomic compare-exchange on the same `u64`, differing only in shift and mask constants -- so the recurrence moves from about 37 seconds to about 20 years without a dependency. **An earlier version of this row added "and measured indistinguishable outside noise", and that clause is withdrawn**: it rested on a single probe run read against a noise floor of 2-6% that seven runs put at 7-61%. Re-measured, the deeper layouts are indistinguishable from `Balanced` at low producer counts, and at high counts sit outside the same-code control but too close to it to establish an ordering or a cost on this host: a flag to measure locally, not a finding. The figures are in the probe's note rather than restated here. The layout is the caller's choice and the throughput question belongs on the caller's hardware; see the queue-contention section of [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-queue-layout-observations). Which half the bits go to is the trade this decision exposes rather than settles: a 32-bit reservation field, whose count the shipping type caps at `u32::MAX` and whose achievable value capacity caps lower still, against a position whose width sets the recurrence horizon. The bound that matters on the reservation side is how many reservations a caller holds at once -- the lesser of the ring capacity and the field, reachable by one producer alone, since `reserve` takes `&self`. Which of the two a deployment needs is the deployment's question. **An earlier version of this row said the real bound was "however many producers are mid-send", and that is withdrawn as false**: one producer fills `Perpetual`'s 255 in a loop and is then refused, which `one_producer_alone_can_exhaust_the_reservation_field` pins. The correction matters because the false premise made the narrower fields look unreachable, which is the argument for spending bits on the position. `Wide` (64/64 over a `u128`) is offered behind the non-default `dwcas` feature, because it is the one thing here that costs a third-party crate -- the standard library has no 128-bit atomic -- and the whole push path was measured as slower under it at every producer count measured in the isolated regime -- smallest at one or two, several times by thirty-two -- while the drained comparison is not settled in either direction, its control having been pooled across producer counts it is not independent of. The probe times the complete push, so that is the layout's effect on that path rather than a measurement of the exchange alone; it buys a longer lifetime argument -- a recurrence at 2^64 rather than at 2^56 -- and not a different kind of argument. Like every horizon in that column it scales with the caller's push rate rather than being absolute. **The default stays `Balanced`** so introducing the choice changed no existing caller's behaviour, and its documentation states the defect outright -- that past 2^32 pushes from two or more producers the queue can silently lose an item -- rather than leaving the status quo to look safe by inertia. It states the defect rather than recommending against the layout, per [D-no-client-prescriptions](../windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions): a caller told what the layout does can decide; a caller told what to prefer has been handed our judgement about their deployment. | ## D-2: capabilities are sliced, not gathered diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 7b3b5267f..7d0666d47 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -161,8 +161,8 @@ let (tx, rx) = reserving_mpsc::bounded_as::(64)?; and differ only in shift and mask constants, so there is no structural reason for one to be slower -- but **what that costs in throughput is not established**: a probe comparing them found them indistinguishable at low producer counts, and at high counts sat outside the probe's same-code control but too close to it to establish an ordering or a cost on this host. `Wide` is a separate matter: it needs a 128-bit exchange, -and the whole push path was measured as slower under it as producer count rises --- near parity at one or two, several times by thirty-two, in the isolated +and the whole push path was measured as slower under it at every producer count +measured -- smallest at one or two, several times by thirty-two, in the isolated regime -- and it is the only thing in this crate that costs a third-party dependency. @@ -237,8 +237,8 @@ stops at 64 bits -- so the double-width compare-and-swap comes from `portable-atomic`. `Perpetual` reaches roughly twenty years before its claim position recurs with no dependency, though what that costs in throughput is not established, while under `Wide` the whole push -path was measured as slower as producer count rises -- near parity at one or -two, several times by thirty-two, in the isolated regime. What `Wide` provides +path was measured as slower at every producer count measured -- smallest at one +or two, several times by thirty-two, in the isolated regime. What `Wide` provides that the `u64` layouts do not is a 64-bit position: the recurrence moves to 2^64 pushes -- about 5,000 years at the same rate the table above uses, rather than the twenty `Perpetual` buys. That is a longer horizon, not the absence of diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 30e02745c..427cf7657 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -134,7 +134,7 @@ //! established**: a probe comparing them found them indistinguishable at low //! producer counts, and at high counts sat outside the probe's same-code control but too close to it to establish an ordering or a cost on this host. `Wide` is a separate //! matter: it needs a 128-bit exchange, and the whole push path was measured as -//! slower under it as producer count rises -- near parity at one or two, +//! slower under it at every producer count measured -- smallest at one or two, //! several times by thirty-two, in the isolated regime -- and it is the only //! thing in //! this crate diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 3f3490d37..0f5a15839 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -624,10 +624,13 @@ impl ClaimLayout for Perpetual { /// documents, against the twenty [`Perpetual`] buys. Longer, not unbounded. /// /// The whole push path was measured as slower under this layout than under a -/// `u64` one, and the difference **grows with producer count** -- near parity -/// at one or two, several times by thirty-two, in the isolated regime on one -/// x86-64 host; against a draining consumer the difference fell inside that -/// host's same-code control and could not be called at all. The probe times the +/// `u64` one at every producer count measured, and the difference **grows with +/// producer count** -- smallest at one or two, several times by thirty-two, in +/// the isolated regime on one x86-64 host. Against a draining consumer the +/// comparison is not settled in either direction: the control there was pooled +/// across producer counts, and it is not independent of producer count, so +/// containment followed from the pooling rather than from the data. The probe +/// times the /// complete push, so this is the layout's effect on that path and not a /// measurement of the 128-bit exchange on its own. The per-count table is in the /// queue-contention section of From 34291893c2fcbf6d08371037e99dfc3303876f8e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 21:15:04 -0400 Subject: [PATCH 097/139] fix(probes): enforce the M4.3 ordering, widen every column, reject malformed cells Five findings from one review round, four of which are the same shape: a rule fixed at the site a reviewer named while its other copies kept the defect. **The M4.3 guarantee was restated four times and enforced nowhere.** The handshake's whole content is that the consumer drains *before* it announces -- announce-first means "about to drain", which a descheduling falsifies. Four drained timers each wrote that ordering by hand, and no test could see any of them, because running a timer runs the whole 70-second probe. Swapping the two lines would have been silent. `drain_then_announce` states it once, all four call sites go through it, and `the_handshake_drains_before_it_announces` drives it with a fake that records what the flag said at the moment the pop ran. Verified by sabotage: swapping the two statements fails the test with the message written for that mutation; before this commit the same swap passed everything. **The corpus checked half the report.** `table_lines` returned the first table whose header matched, and the layout table is emitted twice under identical headers -- once isolated, once drained -- so a width regression in the drained copy passed unseen. It now returns every occurrence and checks each. Doing that surfaced a second defect underneath: headers are matched as substrings, and the report's prose discusses its own columns, so "The atomic floor is the cheapest possible contended operation" matched `atomic floor` and the check compared the line lengths of a paragraph. Measured, not reasoned: the corpus test went red on that sentence the moment all occurrences were checked. `is_columnar` tells a header from prose by the multi-space gaps between fields, and a count assertion pins the layout table at two occurrences so a regime quietly ceasing to be rendered is a failure rather than a smaller suite. **Only the ratio columns had derived widths.** `ns/op range` and `spread` in the per-run table, and the four `ns/op` columns in the layout table, were fixed fields holding rendered measurements -- `spread` is an unbounded quotient, and a long pause is exactly the outlier `median_run` exists to tolerate. `render_table` now renders its cells before sizing its columns, and `column_width` generalises `ratio_column_width` so one definition serves both; the old widths stay as floors, so an ordinary report is byte-identical. **The capture scripts could print NaN.** `summarise.js` routed producer counts and costs through `finite` but read layout ratios with a bare `Number`, and the ratio pattern accepts any run of digits and dots -- so a malformed `...x` cell became NaN, compared false against every guard, and reached the output. Ratios now go through `finite` like their siblings, and a missing drained marker stops the run instead of silently slicing from line 1. `isolated.js` picked the upper-middle value for an even number of runs, reporting the slower of two as their median; it now matches `summarise.js`, which was verified by reading that function rather than assuming it. Both scripts still reproduce their committed outputs byte-identically. Also: `a sink that does too let` -> `lets`. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/isolated.js | 10 +- .../2026-09-16-drained-handshake/summarise.js | 18 ++- .../src/bin/queue_contention/main.rs | 18 ++- .../src/bin/queue_contention/tests.rs | 108 ++++++++++--- .../src/queue_contention.rs | 147 ++++++++++++++++-- .../src/queue_contention/tests.rs | 39 +++++ crates/windows-platform-probes/src/report.rs | 2 +- 7 files changed, 299 insertions(+), 43 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 926177251..06cadfa79 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -71,7 +71,15 @@ function ratios(numerator, denominator) { return out; } -const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)]; +// The middle of an odd set, and the mean of the two middle values otherwise -- +// matching `summarise.js`. The CLI takes any number of runs, and picking the +// upper-middle for an even set would report the slower of two runs as their +// median, which is a different statistic under the same name. +const median = (xs) => { + const sorted = [...xs].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; +}; const fmt = (x) => x.toFixed(2); const cell = (xs) => `${fmt(median(xs))}x [${fmt(Math.min(...xs))}-${fmt(Math.max(...xs))}]`; diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index c9a05294b..835615c80 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -43,6 +43,14 @@ function drainedLayout(lines, path) { lines.forEach((line, i) => { if (line.includes("-- drained --")) start = i; }); + // The raw table's marker is `-- drained: ... --`, which this does not match; + // the one it finds is the claim-layout interpretation table, which is the one + // carrying ratios. A missing marker must stop the run rather than silently + // slice from line 1, which would summarise whatever happened to be there. + if (start < 0) { + problems.push(`${path}: no drained layout table (expected a "-- drained --" marker)`); + return new Map(); + } const rows = new Map(); for (const line of lines.slice(start + 2, start + 8)) { const fields = line.trim().split(/\s+/); @@ -50,7 +58,14 @@ function drainedLayout(lines, path) { if (producers === null) continue; const where = `${path}, drained layout, ${producers} producers`; const narrowNanos = finite(fields[1], `${where}: the 32/32 cost`); - const ratios = [...line.matchAll(RATIO)].map((m) => Number(m[1])); + // Through `finite` like every other captured value. The ratio pattern + // accepts any run of digits and dots, so a malformed cell such as `...x` + // matches, and a bare `Number` would turn it into NaN -- which compares + // false against everything, so it would pass every guard downstream and + // surface as `NaN` in the output rather than as a rejected capture. + const ratios = [...line.matchAll(RATIO)].map((m, i) => + finite(m[1], `${where}: layout ratio ${i + 1}`), + ); // A row that did not run renders `--`, which the ratio pattern does not // match, so a short list is the signal that this row cannot be summarised. if (ratios.length !== 3) { @@ -59,6 +74,7 @@ function drainedLayout(lines, path) { ); continue; } + if (ratios.some((r) => r === null)) continue; if (narrowNanos === null) continue; rows.set(producers, { narrowNanos, ratios }); } diff --git a/crates/windows-platform-probes/src/bin/queue_contention/main.rs b/crates/windows-platform-probes/src/bin/queue_contention/main.rs index 10ec08078..4480e0b9e 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/main.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/main.rs @@ -14,9 +14,9 @@ //! cannot separate. use windows_platform_probes::queue_contention::{ - DRAINED_CAPACITY, Observation, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, format_nanos, - format_ratio_bounded, format_scaling_bounded, measure, ratio_column_width, render_table, - shapes, + DRAINED_CAPACITY, Observation, PRODUCER_COUNTS, PUSHES_PER_PRODUCER, REPETITIONS, column_width, + format_nanos, format_ratio_bounded, format_scaling_bounded, measure, ratio_column_width, + render_table, shapes, }; use windows_platform_probes::report::emit_report; @@ -451,9 +451,17 @@ fn render_observation(out: &mut dyn std::fmt::Write, observation: &Observation) .map(String::as_str) .chain(["16/48 vs", "8/56 vs", "64/64 vs"]), ); + // `format_nanos` renders a measurement, so these columns are no more + // bounded than the ratio columns beside them. + let n = column_width( + rows.iter() + .flat_map(|(_, nanos, _)| nanos) + .map(String::as_str), + 11, + ); let _ = writeln!( out, - " {:<10} {:>11} {:>11} {:>11} {:>11} {:>w$} {:>w$} {:>w$}", + " {:<10} {:>n$} {:>n$} {:>n$} {:>n$} {:>w$} {:>w$} {:>w$}", "producers", "32/32 ns/op", "16/48 ns/op", @@ -466,7 +474,7 @@ fn render_observation(out: &mut dyn std::fmt::Write, observation: &Observation) for (producers, nanos, ratios) in &rows { let _ = writeln!( out, - " {:<10} {:>11} {:>11} {:>11} {:>11} {:>w$} {:>w$} {:>w$}", + " {:<10} {:>n$} {:>n$} {:>n$} {:>n$} {:>w$} {:>w$} {:>w$}", producers, nanos[0], nanos[1], nanos[2], nanos[3], ratios[0], ratios[1], ratios[2], ); } diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs index 2d2019838..5c2ec05d0 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -97,22 +97,56 @@ fn observation_from(value: &Value) -> Observation { } } -/// The lines of the table whose header contains `header`, header included. +/// Every table whose header contains `header`, each with its header included. /// /// A table runs from its header to the first blank line. The drained tables /// carry a second header row, which is part of the table and has to line up /// with the rest of it, so it is not skipped. -fn table_lines<'a>(report: &'a str, header: &str) -> Vec<&'a str> { +/// +/// **All occurrences, not the first.** The report emits the claim-word layout +/// table twice under identical headers -- once isolated, once drained -- and +/// `ns/op range` heads both raw tables. Returning only the first meant the +/// corpus checked the isolated table and a width regression in the drained one +/// passed unseen, which is half the report unguarded. +fn tables<'a>(report: &'a str, header: &str) -> Vec> { let all: Vec<&str> = report.lines().collect(); - let start = all - .iter() - .position(|line| line.contains(header)) - .unwrap_or_else(|| panic!("no table header containing {header:?} in:\n{report}")); - all[start..] - .iter() - .take_while(|line| !line.trim().is_empty()) - .copied() - .collect() + let mut found = Vec::new(); + let mut index = 0; + while index < all.len() { + if all[index].contains(header) && is_columnar(all[index]) { + let table: Vec<&str> = all[index..] + .iter() + .take_while(|line| !line.trim().is_empty()) + .copied() + .collect(); + // Step past this table so its own rows cannot match again. + index += table.len().max(1); + found.push(table); + } else { + index += 1; + } + } + found +} + +/// Whether a line is a table header rather than prose that mentions one. +/// +/// Header names are matched as substrings, and the report's prose discusses the +/// columns it prints -- "The atomic floor is the cheapest possible contended +/// operation" contains `atomic floor` and is a sentence. Slicing from there +/// gathers a paragraph and compares the lengths of its lines, which fails for +/// the ordinary reason that prose is ragged. +/// +/// A header is columnar: its fields are separated by gaps of multiple spaces, so +/// it splits into two or more parts. Prose is single-spaced and splits into one. +/// That one test tells them apart without the fixture having to enumerate +/// either. +fn is_columnar(line: &str) -> bool { + line.trim() + .split(" ") + .filter(|part| !part.is_empty()) + .count() + >= 2 } #[test] @@ -138,13 +172,20 @@ fn every_corpus_case_renders_a_report_whose_tables_line_up() { .expect("`aligned_tables` is an array") { let header = header.as_str().expect("a header is a string"); - let lines = table_lines(&report, header); + let found = tables(&report, header); assert!( - lines.len() > 1, - "[{name}] the table at {header:?} has no rows, so its alignment \ - is not being checked\n{why}" + !found.is_empty(), + "[{name}] no table header containing {header:?} in:\n{why}\n{report}" ); - assert_aligned(name, &lines, why); + for (occurrence, lines) in found.iter().enumerate() { + assert!( + lines.len() > 1, + "[{name}] the table at {header:?} (occurrence {}) has no rows, \ + so its alignment is not being checked\n{why}", + occurrence + 1 + ); + assert_aligned(name, lines, why); + } } for needle in expect["contains"] @@ -187,10 +228,11 @@ fn every_corpus_case_renders_a_report_whose_tables_line_up() { /// test's failure message. #[test] fn the_alignment_check_can_tell_a_misaligned_table_from_an_aligned_one() { - let misaligned = table_lines( + let misaligned = tables( "producers ratio\n1 1.00x [1.00-1.00]\n", "ratio", - ); + ) + .remove(0); assert_eq!(misaligned.len(), 2, "the fixture has a header and one row"); let caught = std::panic::catch_unwind(AssertUnwindSafe(|| { assert_aligned("fixture", &misaligned, "a deliberately overrun table"); @@ -203,6 +245,34 @@ fn the_alignment_check_can_tell_a_misaligned_table_from_an_aligned_one() { misaligned[0].len() ); - let aligned = table_lines("producers ratio\n 1 1.00x\n", "ratio"); + let aligned = tables("producers ratio\n 1 1.00x\n", "ratio").remove(0); assert_aligned("fixture", &aligned, "an aligned table must not be reported"); } + +/// The layout table is rendered for both regimes, and both are checked. +/// +/// Returning every occurrence only helps if there are two to find. Were the +/// drained layout table to stop being rendered, every alignment assertion above +/// would still pass -- there would simply be one fewer table to check, which is +/// silence rather than failure. This pins the count so the disappearance is a +/// test failure instead of a quietly smaller suite. +#[test] +fn an_ordinary_observation_renders_the_layout_table_for_both_regimes() { + let corpus: Value = serde_json::from_str(CORPUS).expect("the corpus parses"); + let case = corpus["cases"] + .as_array() + .expect("`cases` is an array") + .iter() + .find(|case| case["name"] == "ordinary") + .expect("the corpus has an `ordinary` case"); + + let mut report = String::new(); + render_observation(&mut report, &observation_from(&case["observation"])); + + assert_eq!( + tables(&report, "16/48 vs").len(), + 2, + "the claim-word layout table is rendered once isolated and once drained, \ + so a count other than two means a regime stopped being reported\n{report}" + ); +} diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index f9566f614..787c25bfc 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -491,11 +491,23 @@ pub const RATIO_COLUMN_WIDTH: usize = 20; /// emitting the header, which is the only ordering that can get this right. #[must_use] pub fn ratio_column_width<'a>(cells: impl IntoIterator) -> usize { + column_width(cells, RATIO_COLUMN_WIDTH) +} + +/// The width a column must take to keep its rows aligned with its header. +/// +/// The widest cell, or `floor` when that is wider. Every column in this report +/// holds rendered measurements, whose length is a function of the data rather +/// than a constant, so a fixed field silently shifts everything to its right the +/// first time a value outgrows it. `floor` only stops a table of narrow values +/// from looking cramped; it is never an upper bound. +#[must_use] +pub fn column_width<'a>(cells: impl IntoIterator, floor: usize) -> usize { cells .into_iter() .map(str::len) .max() - .map_or(RATIO_COLUMN_WIDTH, |widest| widest.max(RATIO_COLUMN_WIDTH)) + .map_or(floor, |widest| widest.max(floor)) } /// Renders a scaling factor together with the interval it could occupy. @@ -526,11 +538,25 @@ pub fn format_scaling_bounded(point: Option, bounds: Option<(f64, f64)>) -> /// composes a string emits its lines first, reordering the report without losing /// any of it. pub fn render_table(out: &mut dyn fmt::Write, runs: &[Run]) { - let _ = writeln!( - out, - "{:<18} {:>10} {:>14} {:>16} {:>14} {:>18} {:>9}", - "shape", "producers", "ns/op", "ops/sec", "refusals", "ns/op range", "spread" - ); + const HEADERS: [&str; 7] = [ + "shape", + "producers", + "ns/op", + "ops/sec", + "refusals", + "ns/op range", + "spread", + ]; + // The widths this table has always used, kept as floors so an ordinary + // report is unchanged. Every column is a measurement rendered at its natural + // width, and `spread` and `ns/op range` are quotients and pairs of measured + // endpoints with no upper bound -- a long pause, the very outlier + // `median_run` exists to tolerate, renders wider than any fixed field and + // pushes every column after it out of line. See `ratio_column_width`, which + // is the same argument for the ratio tables. + const FLOOR: [usize; 7] = [18, 10, 14, 16, 14, 18, 9]; + + let mut rows: Vec<[String; 7]> = Vec::with_capacity(runs.len()); for run in runs { // `shape` and `producers` are configuration and always mean something. // Every other column is a measurement, so a row that did not run has @@ -557,10 +583,60 @@ pub fn render_table(out: &mut dyn fmt::Write, runs: &[Run]) { "--".to_owned(), ) }; + rows.push([ + run.shape.to_owned(), + run.producers.to_string(), + nanos, + ops, + refusals, + range, + spread, + ]); + } + + let mut width = FLOOR; + for row in &rows { + for (column, cell) in row.iter().enumerate() { + width[column] = width[column].max(cell.len()); + } + } + + let _ = writeln!( + out, + "{:b$} {:>c$} {:>d$} {:>e$} {:>f$} {:>g$}", + HEADERS[0], + HEADERS[1], + HEADERS[2], + HEADERS[3], + HEADERS[4], + HEADERS[5], + HEADERS[6], + a = width[0], + b = width[1], + c = width[2], + d = width[3], + e = width[4], + f = width[5], + g = width[6], + ); + for row in &rows { let _ = writeln!( out, - "{:<18} {:>10} {:>14} {:>16} {:>14} {:>18} {:>9}", - run.shape, run.producers, nanos, ops, refusals, range, spread, + "{:b$} {:>c$} {:>d$} {:>e$} {:>f$} {:>g$}", + row[0], + row[1], + row[2], + row[3], + row[4], + row[5], + row[6], + a = width[0], + b = width[1], + c = width[2], + d = width[3], + e = width[4], + f = width[5], + g = width[6], ); } } @@ -1127,6 +1203,29 @@ fn await_consumer(ready: &AtomicBool) { } } +/// Drains once, then announces -- the producing half of the `M4.3` handshake. +/// +/// **The one statement of the ordering.** Four drained timers need it, and a +/// hand-written `pop` followed by a `store` in each of them is four chances for +/// the two lines to end up the other way round, which no test could see: the +/// timers cannot run without running the whole probe. Defining it here gives the +/// ordering a single home that [`the_handshake_drains_before_it_announces`] can +/// drive with a recording fake. +/// +/// Swapping these two statements reintroduces the undrained opening in its +/// narrower form -- the announcement would mean "this consumer is about to +/// drain", which a descheduling can falsify, rather than "this consumer has +/// executed the pop path", which nothing can. +/// +/// `Release` pairs with the `Acquire` in [`await_consumer`], so a producer that +/// observes the flag has the pop ordered before it. +/// +/// [`the_handshake_drains_before_it_announces`]: crate::queue_contention::tests +fn drain_then_announce(pop_once: impl FnOnce(), ready: &AtomicBool) { + pop_once(); + ready.store(true, Ordering::Release); +} + fn time_drained_mpsc(producers: usize) -> Repetition { let (tx, rx) = slotwise_mpsc::bounded::(DRAINED_CAPACITY).expect("a valid capacity"); let done = Arc::new(AtomicBool::new(false)); @@ -1164,8 +1263,12 @@ fn time_drained_mpsc(producers: usize) -> Repetition { // announcement mean `this consumer has executed the pop path`, which is a // fact rather than an intention. The queue is empty here, so it costs one // failed pop, and it happens before any producer has started timing. - let _ = rx.pop(); - consumer_ready.store(true, Ordering::Release); + drain_then_announce( + || { + let _ = rx.pop(); + }, + &consumer_ready, + ); // Spin rather than park: the doorbell's cost is `doorbell_cost`'s // question, and parking here would measure that instead of the claim. while !consumer_done.load(Ordering::Relaxed) { @@ -1272,8 +1375,12 @@ fn time_drained_reserving(producers: usize) -> Repetition { // announcement mean `this consumer has executed the pop path`, which is a // fact rather than an intention. The queue is empty here, so it costs one // failed pop, and it happens before any producer has started timing. - let _ = rx.pop(); - consumer_ready.store(true, Ordering::Release); + drain_then_announce( + || { + let _ = rx.pop(); + }, + &consumer_ready, + ); while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} std::hint::spin_loop(); @@ -1359,8 +1466,12 @@ fn time_drained_permit(producers: usize) -> Repetition { // announcement mean `this consumer has executed the pop path`, which is a // fact rather than an intention. The queue is empty here, so it costs one // failed pop, and it happens before any producer has started timing. - let _ = rx.pop(); - consumer_ready.store(true, Ordering::Release); + drain_then_announce( + || { + let _ = rx.pop(); + }, + &consumer_ready, + ); while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} std::hint::spin_loop(); @@ -1498,8 +1609,12 @@ fn time_drained_layout(producers: usize) -> Repetition // announcement mean `this consumer has executed the pop path`, which is a // fact rather than an intention. The queue is empty here, so it costs one // failed pop, and it happens before any producer has started timing. - let _ = rx.pop(); - consumer_ready.store(true, Ordering::Release); + drain_then_announce( + || { + let _ = rx.pop(); + }, + &consumer_ready, + ); while !consumer_done.load(Ordering::Relaxed) { while rx.pop().is_ok() {} std::hint::spin_loop(); diff --git a/crates/windows-platform-probes/src/queue_contention/tests.rs b/crates/windows-platform-probes/src/queue_contention/tests.rs index 96cba17de..76f2b04e1 100644 --- a/crates/windows-platform-probes/src/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/queue_contention/tests.rs @@ -1565,3 +1565,42 @@ fn a_producer_waits_for_the_consumer_to_announce_that_it_is_draining() { thread::sleep(Duration::from_millis(10)); } } + +/// The handshake drains before it announces, not after. +/// +/// The test above proves a producer waits for the announcement. It says nothing +/// about what the announcement means, so it would pass just as happily if the +/// consumer announced first and drained second -- and that ordering is the whole +/// of `M4.3`. Announcing first makes the flag mean "about to drain", which a +/// descheduling between the store and the first `pop` falsifies; draining first +/// makes it mean "has executed the pop path", which nothing can. +/// +/// Reachable only because [`drain_then_announce`] states the ordering once. The +/// four drained timers that use it cannot be tested directly -- running one runs +/// the whole probe -- so a hand-written `pop`-then-`store` in each was four +/// copies of a guarantee nothing could check. +/// +/// The fake records what the flag said *at the moment the pop ran*. If the store +/// had already happened, it sees `true`. +#[test] +fn the_handshake_drains_before_it_announces() { + let ready = AtomicBool::new(false); + let already_announced = AtomicBool::new(false); + + drain_then_announce( + || already_announced.store(ready.load(Ordering::Acquire), Ordering::Release), + &ready, + ); + + assert!( + !already_announced.load(Ordering::Acquire), + "readiness was published before the consumer drained, so a producer \ + released by it can push into a queue whose consumer has not run -- the \ + undrained opening M4.3 closed, in its narrower form" + ); + assert!( + ready.load(Ordering::Acquire), + "the handshake drained but never announced, so every producer would spin \ + forever in await_consumer" + ); +} diff --git a/crates/windows-platform-probes/src/report.rs b/crates/windows-platform-probes/src/report.rs index ca7807a59..b81bb4424 100644 --- a/crates/windows-platform-probes/src/report.rs +++ b/crates/windows-platform-probes/src/report.rs @@ -98,7 +98,7 @@ impl Captured { /// `writeln!(out, ...)` against a `String`; a sink method taking /// `fmt::Arguments` would have been explicit but would have rewritten every one /// of those sites, while `String` already implements `fmt::Write` -- so a sink -/// that does too let every write site stand untouched and moved only the +/// that does too lets every write site stand untouched and moved only the /// renderer signatures, which now take `&mut dyn fmt::Write` and are handed a /// [`LineSink`] by `emit_report_to`. /// From 5665a0e29368e79154436056937e2f98c2b98029 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 21:15:05 -0400 Subject: [PATCH 098/139] docs(queue): correct the 32-bit lap to seconds, and the wrap to a queue's lifetime Two claims that disagreed with arithmetic this crate publishes itself. **"Minutes" is thirty-seven seconds.** `permit_mpsc` said a 32-bit counter "laps in minutes at the reference rate", and `slotwise_mpsc` called the same interval "a matter of minutes". At the rate `ClaimLayout` documents, 2^32 / 116e6 = 37.0 seconds -- computed, not estimated. Both sentences exist to justify choosing a 64-bit position, and overstating the horizon by roughly five times weakens the argument they are making while contradicting the table a reader can see. **The wrap is cumulative.** The crate doc's exposure bullet still asked about "~4 billion items in one run" after the README's copy was corrected: the same proposition, one site short, which is the defect this branch keeps repeating. The position advances over the queue's whole life, so many short bursts reach the wrap as surely as one long one, and the run-scoped question let a reader who never has a long run conclude the hazard was not theirs. Swept: no other site states it. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/src/lib.rs | 6 ++++-- crates/windows-waitable-queues/src/permit_mpsc.rs | 2 +- crates/windows-waitable-queues/src/slotwise_mpsc.rs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 427cf7657..ea3cd703d 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -304,9 +304,11 @@ //! answered at all. Both are well-studied designs in production use elsewhere, //! which is why this crate ships both instead of picking one for you. //! -//! - **Pushing more than ~4 billion items in one run, from two or more +//! - **Pushing more than ~4 billion items through one queue, from two or more //! producers?** [`reserving_mpsc`] under its default layout has a known -//! item-loss defect past that volume, on every target; [`slotwise_mpsc`]'s +//! item-loss defect past that volume, on every target. The count is cumulative +//! over that queue's whole life, not per run: many short bursts reach the wrap +//! as surely as one long one. [`slotwise_mpsc`]'s //! positions are 64 bits under every configuration, and naming a deeper layout //! on [`reserving_mpsc`] moves the recurrence out. The mechanism is in the //! section above. diff --git a/crates/windows-waitable-queues/src/permit_mpsc.rs b/crates/windows-waitable-queues/src/permit_mpsc.rs index 9bfa6c14a..041250ca4 100644 --- a/crates/windows-waitable-queues/src/permit_mpsc.rs +++ b/crates/windows-waitable-queues/src/permit_mpsc.rs @@ -78,7 +78,7 @@ use crate::metrics::Metrics; /// /// **64 bits on every target, deliberately, rather than `usize`**, for the same /// reason [`slotwise_mpsc`](crate::slotwise_mpsc) made the same choice: a -/// 32-bit counter laps in minutes at the reference rate +/// 32-bit counter laps in about thirty-seven seconds at the reference rate /// [`reserving_mpsc::ClaimLayout`] documents -- an arithmetic input taken from /// another shape rather than a bound on this one -- and a shape /// whose soundness depends on the target's pointer width is not one this crate diff --git a/crates/windows-waitable-queues/src/slotwise_mpsc.rs b/crates/windows-waitable-queues/src/slotwise_mpsc.rs index 2c6717800..65e0eea0e 100644 --- a/crates/windows-waitable-queues/src/slotwise_mpsc.rs +++ b/crates/windows-waitable-queues/src/slotwise_mpsc.rs @@ -84,7 +84,7 @@ use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; /// counter cannot lap. /// /// With `usize` it can. On a 32-bit target the counter laps after 2^32 claims, -/// which is a matter of minutes at the reference rate +/// which is about thirty-seven seconds at the reference rate /// [`reserving_mpsc::ClaimLayout`] documents. **That rate is `reserving_mpsc`'s, /// and is used here only as an arithmetic input rather than as a bound on this /// shape**: this shape's own measured throughput differs, and at low producer From 9e9358835611fcb4cd01f33fb858e54e0a304484 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 21:27:52 -0400 Subject: [PATCH 099/139] fix(probes): validate the capture's producer set, and derive the summary widths Three findings, plus a correction I introduced last round. **The completeness check was circular.** `summarise.js` derived the expected producer set from the first capture and then checked the others against it, so three runs all truncated at the same producer count agreed with each other, left `rows` non-empty, and were reported as a whole capture. The sweep is now stated (`EXPECTED_PRODUCERS`) and every run checked against it. Verified by sabotage, and the sabotage had to be built carefully to test the right thing: relabelling one run's 32-producer row is caught by the existing cross-table check, which is not this guard. Relabelling *all three* identically, in both the layout and comparison tables, is the case only this guard can see -- and it reports all three runs and exits 1, where before it would have summarised five producer counts and called them six. **Two columns were fixed-width over unbounded values** -- the control band in `summarise.js` and every column in `isolated.js`, both built from measured ratios. Same argument as `column_width` in the Rust report, and the same remedy: derive from the rendered cells, keeping the old widths as floors so both committed outputs still reproduce byte-identically. **`isolated.js` was missing the copyright line.** Added. **`report.rs`: `lets ... and moved` -> `moves`.** I introduced that disagreement last round by accepting a grammar finding without reading the whole sentence: the original `let ... and moved` was consistent past tense, and changing one verb broke it. Both are present tense now, which is unambiguous and agrees with the `which now take` that follows. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/isolated.js | 33 ++++++++++++++----- .../2026-09-16-drained-handshake/summarise.js | 33 +++++++++++++++---- crates/windows-platform-probes/src/report.rs | 2 +- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 06cadfa79..943083c20 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -1,3 +1,5 @@ +// Copyright (c) Mike Grier. + // Isolated regime: each claim-word layout against the default, read against a // same-code control. // @@ -86,19 +88,34 @@ const cell = (xs) => `${fmt(median(xs))}x [${fmt(Math.min(...xs))}-${fmt(Math.ma const control = ratios(DEFAULT_LAYOUT, CONTROL_TWIN); const measured = new Map(LAYOUTS.map((l) => [l, ratios(l, DEFAULT_LAYOUT)])); +// Widths derived from the cells, not fixed. `cell()` renders a median and a +// range of measured ratios, which have no upper bound, so a fixed field shifts +// every column after it the first time a value outgrows it -- the same argument +// `column_width` makes for the Rust report's columns. +const COLUMNS = ["control", ...LAYOUTS.map((l) => l.replace("reserving", ""))]; +const body = COUNTS.map((n) => [cell(control.get(n)), ...LAYOUTS.map((l) => cell(measured.get(l).get(n)))]); +// The widths this table has always used, kept as floors so the committed +// output is unchanged; the derivation only ever widens. +const width = COLUMNS.map((name, column) => + Math.max(18, name.length, ...body.map((row) => row[column].length)), +); +const PRODUCERS_WIDTH = Math.max(9, ...COUNTS.map((n) => String(n).length)); + console.log(`isolated regime, ${files.length} run(s): ${files.join(", ")}`); console.log(`each layout against ${DEFAULT_LAYOUT}; control is ${DEFAULT_LAYOUT} against ${CONTROL_TWIN}`); console.log("median of the per-run ratios, with the observed range beside it\n"); -const head = ["producers".padEnd(11), "control".padEnd(20)].concat( - LAYOUTS.map((l) => l.replace("reserving", "").padEnd(20)), +console.log( + ["producers".padEnd(PRODUCERS_WIDTH + 2), ...COLUMNS.map((name, i) => name.padEnd(width[i] + 2))].join(""), ); -console.log(head.join("")); -for (const n of COUNTS) { - const row = [String(n).padEnd(11), cell(control.get(n)).padEnd(20)]; - for (const l of LAYOUTS) row.push(cell(measured.get(l).get(n)).padEnd(20)); - console.log(row.join("")); -} +COUNTS.forEach((n, row) => { + console.log( + [ + String(n).padEnd(PRODUCERS_WIDTH + 2), + ...body[row].map((c, i) => c.padEnd(width[i] + 2)), + ].join(""), + ); +}); console.log("\nwhere every run sat above the control's whole observed range:"); for (const l of LAYOUTS) { diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 835615c80..358962d3a 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -125,7 +125,22 @@ for (const path of paths) { controls.push(control); } -const producers = [...layouts[0].keys()].sort((a, b) => a - b); +// The sweep the probe runs, stated rather than inferred. Deriving the expected +// set from the first capture makes the completeness check circular: three runs +// all truncated at the same producer count agree with each other, `rows` stays +// non-empty, and the script reports a partial capture as a whole one. +const EXPECTED_PRODUCERS = [1, 2, 4, 8, 16, 32]; + +layouts.forEach((layout, i) => { + const seen = [...layout.keys()].sort((a, b) => a - b); + if (seen.join(",") !== EXPECTED_PRODUCERS.join(",")) { + problems.push( + `${paths[i]}: drained layout covers producers [${seen}], expected [${EXPECTED_PRODUCERS}]`, + ); + } +}); + +const producers = EXPECTED_PRODUCERS; console.log(`runs: ${paths.length}`); // **Reported per producer count, and deliberately without a verdict.** @@ -186,16 +201,22 @@ console.log( ); console.log("each layout's median ratio against 32/32, across runs."); console.log(""); -console.log("producers control(n) 16/48 8/56 64/64"); -for (const row of rows) { +// Derived, with the table's original width as a floor: a control band is built +// from measured ratios and has no upper bound, so a fixed field would shift the +// layout columns the first time one outgrew it. +const bands = rows.map((row) => { const low = Math.min(...row.control); const high = Math.max(...row.control); - const band = `${low.toFixed(2)}-${high.toFixed(2)}(${row.control.length})`; + return `${low.toFixed(2)}-${high.toFixed(2)}(${row.control.length})`; +}); +const bandWidth = Math.max(16, "control(n)".length, ...bands.map((b) => b.length)); +console.log(`producers ${"control(n)".padEnd(bandWidth)} 16/48 8/56 64/64`); +rows.forEach((row, i) => { console.log( - `${String(row.producers).padStart(9)} ${band.padEnd(16)} ` + + `${String(row.producers).padStart(9)} ${bands[i].padEnd(bandWidth)} ` + row.medians.map((m) => `${m.toFixed(2)}x`).join(" "), ); -} +}); const everyControl = rows.flatMap((row) => row.control); console.log(""); diff --git a/crates/windows-platform-probes/src/report.rs b/crates/windows-platform-probes/src/report.rs index b81bb4424..fbbec13b0 100644 --- a/crates/windows-platform-probes/src/report.rs +++ b/crates/windows-platform-probes/src/report.rs @@ -98,7 +98,7 @@ impl Captured { /// `writeln!(out, ...)` against a `String`; a sink method taking /// `fmt::Arguments` would have been explicit but would have rewritten every one /// of those sites, while `String` already implements `fmt::Write` -- so a sink -/// that does too lets every write site stand untouched and moved only the +/// that does too lets every write site stand untouched and moves only the /// renderer signatures, which now take `&mut dyn fmt::Write` and are handed a /// [`LineSink`] by `emit_report_to`. /// From c1d7d1506bf9b6ab363925cfaeb833d41678edb8 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 21:27:52 -0400 Subject: [PATCH 100/139] docs: separate "no findings in this class" from "the instrument has not run" The convergence note said TLA+ and `loom` "target algorithm properties, which have produced no findings". The intended subject was the *class* -- algorithm properties produced no review findings across PR #90 -- but juxtaposing the two tool names with "have produced no findings" reads as the tools having run and come back clean. Neither has run: D-31 records the `loom` verification as planned and `M31.6` still owns it. The distinction is the whole point of the paragraph, which argues about where to aim an expensive instrument. Claiming a clean result from an instrument that has not been used would be the strongest possible version of the defect this document exists to catalogue. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 4099e7d44..33754a777 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1943,8 +1943,11 @@ corrected in its own measurements more than once. Three conclusions, of which the middle one is the one that changes practice. **Formal specification and prose reduction address different classes.** TLA+ and `loom` -([D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31)) target algorithm properties, which -have produced no findings and carry one known unfound defect. Restatement targets documented facts, +([D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31)) target algorithm properties. Neither +has been run -- D-31 records the `loom` verification as planned, and `M31.6` still owns it -- so +what can be said about that class is that it produced no findings in any review round of PR #90 +while carrying one known unfound defect, which is a statement about the reviews rather than a +result from either instrument. Restatement targets documented facts, which have produced most findings. Both are worth doing; conflating them would aim the expensive instrument at the cheap problem. From 78899e0f274842c1cfd799966fc8636b9f4e5f5a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 21:40:33 -0400 Subject: [PATCH 101/139] test(probes): pin how many tables each corpus header must find `aligned_tables` was a list of headers and the check was "at least one matched". Last round's fix made every occurrence get checked, which closed a width regression hiding in the drained copy -- but it left the complementary hole open: deleting a table entirely still passed, because there was simply one fewer thing to check. Silence, not failure. It is now a map from header to the number of tables the report must head with it. The raw table and the claim-layout table are each rendered twice, once per regime, so the counts are 2, and the two interpretation tables are 1. Measured rather than assumed -- a throwaway test printed the real counts per case before the corpus was written to claim them. Verified by sabotage: removing the isolated raw table now fails with "the report has 1 table(s) headed \"ns/op range\", expected 2". The same deletion passed before this commit. `nothing_measured_at_all` keeps `ns/op range` in `contains` rather than in the map, and the schema now records why: when nothing was measured the raw table is still headed but carries no rows, and a header aligns with itself, so listing it there would buy a vacuous assertion. That is also the answer to why the original corpus omitted it -- the exclusion was deliberate and went unexplained, which is how it came to look like an oversight. Removes `an_ordinary_observation_renders_the_layout_table_for_both_regimes`, which pinned one header on one case and is subsumed: the corpus now pins every header on every case, from data rather than from a hand-written assertion. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention/corpus.json | 24 ++++++--- .../src/bin/queue_contention/tests.rs | 51 ++++++------------- 2 files changed, 32 insertions(+), 43 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention/corpus.json b/crates/windows-platform-probes/src/bin/queue_contention/corpus.json index 1d05d1001..e45442f26 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/corpus.json +++ b/crates/windows-platform-probes/src/bin/queue_contention/corpus.json @@ -6,11 +6,21 @@ "were unreachable by the suite until M4.7, and two defects shipped through", "that gap.", "", - "`aligned_tables` names a header substring. Every non-blank line of that", - "table, header included, must be the same length. That is exactly the", + "`aligned_tables` maps a header substring to how many tables the report must", + "head with it. Every non-blank line of each of those tables, header included,", + "must be the same length. That is exactly the", "property a cell wider than its column breaks, and it is derived from the", "output rather than restated as a golden -- so it catches a width bug the", - "corpus never anticipated, which a golden cannot." + "corpus never anticipated, which a golden cannot.", + "The count is part of the expectation rather than a minimum: the raw table", + "and the claim-layout table are each rendered twice, once per regime, under", + "identical headers. Checking only that at least one matched would let a", + "regime stop being reported without any assertion failing -- there would", + "simply be one fewer table to check, which is silence rather than failure.", + "A table that is legitimately empty is named in `contains` instead of here:", + "when nothing was measured the raw table is still headed but has no data", + "rows, and a header on its own always aligns with itself, so listing it", + "would buy a vacuous assertion rather than a check." ], "cases": [ { @@ -50,7 +60,7 @@ ] }, "expect": { - "aligned_tables": ["16/48 vs", "reserving/slotwise", "atomic floor", "ns/op range"], + "aligned_tables": { "16/48 vs": 2, "reserving/slotwise": 1, "atomic floor": 1, "ns/op range": 2 }, "contains": ["processors available to this process: 8"], "absent": [] } @@ -92,7 +102,7 @@ ] }, "expect": { - "aligned_tables": ["16/48 vs", "reserving/slotwise", "atomic floor", "ns/op range"], + "aligned_tables": { "16/48 vs": 2, "reserving/slotwise": 1, "atomic floor": 1, "ns/op range": 2 }, "contains": ["100.00x [6.67-22500.00]"], "absent": [] } @@ -121,7 +131,7 @@ ] }, "expect": { - "aligned_tables": ["16/48 vs", "reserving/slotwise", "atomic floor", "ns/op range"], + "aligned_tables": { "16/48 vs": 2, "reserving/slotwise": 1, "atomic floor": 1, "ns/op range": 2 }, "contains": ["--"], "absent": ["0.00x", " 0.0 ", "infx", "NaN"] } @@ -135,7 +145,7 @@ "drained": [] }, "expect": { - "aligned_tables": ["16/48 vs", "reserving/slotwise", "atomic floor"], + "aligned_tables": { "16/48 vs": 2, "reserving/slotwise": 1, "atomic floor": 1 }, "contains": [ "processors available to this process: unknown (the query failed)", "ns/op range" diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs index 5c2ec05d0..2d3c23fb6 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -167,15 +167,22 @@ fn every_corpus_case_renders_a_report_whose_tables_line_up() { render_observation(&mut report, &observation); let expect = &case["expect"]; - for header in expect["aligned_tables"] - .as_array() - .expect("`aligned_tables` is an array") - { - let header = header.as_str().expect("a header is a string"); + let aligned = expect["aligned_tables"] + .as_object() + .expect("`aligned_tables` maps a header to how many times it must appear"); + for (header, count) in aligned { + let expected = count + .as_u64() + .expect("an expected occurrence count is a number") + as usize; let found = tables(&report, header); - assert!( - !found.is_empty(), - "[{name}] no table header containing {header:?} in:\n{why}\n{report}" + assert_eq!( + found.len(), + expected, + "[{name}] the report has {} table(s) headed {header:?}, expected {expected}. \ + A count that has dropped means a table stopped being rendered, which every \ + alignment assertion below would otherwise pass in silence.\n{why}\n{report}", + found.len() ); for (occurrence, lines) in found.iter().enumerate() { assert!( @@ -248,31 +255,3 @@ fn the_alignment_check_can_tell_a_misaligned_table_from_an_aligned_one() { let aligned = tables("producers ratio\n 1 1.00x\n", "ratio").remove(0); assert_aligned("fixture", &aligned, "an aligned table must not be reported"); } - -/// The layout table is rendered for both regimes, and both are checked. -/// -/// Returning every occurrence only helps if there are two to find. Were the -/// drained layout table to stop being rendered, every alignment assertion above -/// would still pass -- there would simply be one fewer table to check, which is -/// silence rather than failure. This pins the count so the disappearance is a -/// test failure instead of a quietly smaller suite. -#[test] -fn an_ordinary_observation_renders_the_layout_table_for_both_regimes() { - let corpus: Value = serde_json::from_str(CORPUS).expect("the corpus parses"); - let case = corpus["cases"] - .as_array() - .expect("`cases` is an array") - .iter() - .find(|case| case["name"] == "ordinary") - .expect("the corpus has an `ordinary` case"); - - let mut report = String::new(); - render_observation(&mut report, &observation_from(&case["observation"])); - - assert_eq!( - tables(&report, "16/48 vs").len(), - 2, - "the claim-word layout table is rendered once isolated and once drained, \ - so a count other than two means a regime stopped being reported\n{report}" - ); -} From 508e312a85934a0d4dd839c27dffd8a3708e399a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 21:46:57 -0400 Subject: [PATCH 102/139] fix(probes): read the interval rather than the point, and reject non-positive cells **The report told the reader to draw a verdict from a point estimate.** The paragraph under the comparison table said "below 1.00 and the safer claim is also the cheaper one; above 1.00 and closing the hole costs throughput" -- while the column it describes renders an interval, and in the committed capture the 32-producer permit/reserving cell is `0.78x [0.53-1.16]`, which crosses 1.00. A reader following that instruction concludes the safer claim is cheaper from a measurement that does not order the two. It now directs the reader to the interval, says a crossing interval leaves the ordering unsettled however far the point sits from 1.00, and names that cell as the worked example. **`summarise.js` accepted values that are finite but impossible.** Its ratios are costs divided by costs, so zero and negative are as invalid as NaN -- and more dangerous, because `0.00x` renders as an ordinary measurement, and `0.0` is exactly the shape the probe's old did-not-run sentinel took. `positive` now guards every captured quantity in both scripts, including `isolated.js`, whose guard checked the denominator but let a zero numerator divide cleanly. **The ratio pattern matched things that are not numbers.** `[0-9.]+` accepts a run of dots, so a malformed `...x [..-..]` parsed and became NaN. Tightened to `\d+\.\d+`, and all three numbers of every cell are now checked rather than the median alone -- the two bounds are unused by this script, but a capture with an unreadable bound is not one it should certify as summarised. Verified by sabotage: a `0.00x` ratio reports `"0.00" is not positive`, and a `[0.96-....]` bound reports `found 2 layout ratios, expected 3`. Both were accepted silently before. Both committed outputs still reproduce byte-identically. The capture README no longer claims the banner label is the only way the runs differ from a fresh invocation, since this commit adds a second one. It says what the class is -- prose the probe prints around its tables -- and that the figures rather than the surrounding text are what to compare. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 3 +- .../2026-09-16-drained-handshake/README.md | 12 +++-- .../2026-09-16-drained-handshake/isolated.js | 6 ++- .../2026-09-16-drained-handshake/summarise.js | 48 +++++++++++++------ .../src/bin/queue_contention/main.rs | 16 ++++++- 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 876670ead..96aac07b8 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -793,7 +793,8 @@ branch was measured as though it were the algorithm. The reasoning was that both layouts issue the same `lock cmpxchg` on the same `u64`, so only the shift and mask constants differ, and the table above was read as confirming it. The table cannot carry that weight: these are single-run -figures, and the same-code control measured later ranges 0.68-1.27x, which is +figures, and the same-code control measured later ranges 0.69-1.12x isolated and +0.68-1.27x drained, either of which is wider than most of the differences being called "noise" -- note that this very table has 16/48 at 1.14x and 1.21x while the prose beneath it says "within noise". See diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md index 4918d7902..b498d3f6f 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md @@ -30,10 +30,16 @@ are comparable; nothing here says anything about any other hardware, and the banner's `numa[16]` is a single node holding all sixteen processors. The runs' regime banner reads `a consumer popping continuously`, which is what -`68198359` printed. A later commit changed that label to `a consumer looping on +`68198359` printed; a later commit changed that label to `a consumer looping on pop`, because the handshake guarantees the consumer's pop path has run once, not -that it is scheduled without gaps. The label is the only difference: the runs -below are not retaken for it, since nothing about what was measured moved. +that it is scheduled without gaps. A later commit also rewrote the paragraph +under the comparison table, which used to read the point estimate as a verdict +and now directs the reader to the interval. + +Both are prose the probe prints around its tables, not measurements, so the runs +below are not retaken for them: nothing about what was measured moved. Expect the +committed runs to differ from a fresh one in wording of this kind, and compare +the figures rather than the surrounding text. ## Reading it diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 943083c20..c189b3090 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -63,7 +63,11 @@ function ratios(numerator, denominator) { const b = t.get(`${denominator}@${n}`); // A missing or unusable row is an error, not a skipped count: silently // dropping one would quietly narrow every range printed below. - if (!Number.isFinite(a) || !Number.isFinite(b) || b <= 0) { + // Both operands must be positive, not merely finite and the denominator + // non-zero: a zero or negative numerator divides cleanly and publishes + // an ordinary-looking `0.00x`. The probe's did-not-run sentinel used to + // be `0.0`, so that is the shape a legacy capture actually takes. + if (!Number.isFinite(a) || !Number.isFinite(b) || a <= 0 || b <= 0) { throw new Error(`${files[i]}: no usable ${numerator}/${denominator} at ${n} producers`); } return a / b; diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 358962d3a..726dab896 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -8,7 +8,11 @@ const fs = require("fs"); -const RATIO = /([0-9.]+)x \[([0-9.]+)-([0-9.]+)\]/g; +// A ratio cell, with all three numbers required to be plain decimals. The +// looser `[0-9.]+` also matched a run of dots, so a malformed `...x [..-..]` +// parsed and `Number("...")` became NaN -- which compares false against every +// guard downstream and would surface in the output rather than be rejected. +const RATIO = /(\d+\.\d+)x \[(\d+\.\d+)-(\d+\.\d+)\]/g; function median(values) { const sorted = [...values].sort((a, b) => a - b); @@ -37,6 +41,21 @@ function finite(text, what) { return value; } +// Every quantity this script reads is a cost or a ratio of costs, so zero and +// negative are as invalid as NaN -- and more dangerous, because `0.00x` is +// finite and renders as an ordinary-looking measurement. The probe's own +// did-not-run sentinel used to be `0.0`, so this is the shape a legacy or +// malformed capture actually takes. +function positive(text, what) { + const value = finite(text, what); + if (value === null) return null; + if (value <= 0) { + problems.push(`${what}: ${JSON.stringify(text)} is not positive`); + return null; + } + return value; +} + // producers -> { narrowNanos, ratios: [16/48, 8/56, 64/64] } function drainedLayout(lines, path) { let start = -1; @@ -57,26 +76,27 @@ function drainedLayout(lines, path) { const producers = finite(fields[0], `${path}: a drained layout producer count`); if (producers === null) continue; const where = `${path}, drained layout, ${producers} producers`; - const narrowNanos = finite(fields[1], `${where}: the 32/32 cost`); - // Through `finite` like every other captured value. The ratio pattern - // accepts any run of digits and dots, so a malformed cell such as `...x` - // matches, and a bare `Number` would turn it into NaN -- which compares - // false against everything, so it would pass every guard downstream and - // surface as `NaN` in the output rather than as a rejected capture. - const ratios = [...line.matchAll(RATIO)].map((m, i) => - finite(m[1], `${where}: layout ratio ${i + 1}`), - ); + const narrowNanos = positive(fields[1], `${where}: the 32/32 cost`); + // Through `positive` like every other captured value, and all three numbers + // of each cell are checked -- the two bounds are not used by this script, + // but a capture carrying an unreadable bound is not a capture this script + // should certify as summarised. + const ratios = [...line.matchAll(RATIO)].flatMap((m, i) => [ + positive(m[1], `${where}: layout ratio ${i + 1}`), + positive(m[2], `${where}: layout ratio ${i + 1} lower bound`), + positive(m[3], `${where}: layout ratio ${i + 1} upper bound`), + ]); // A row that did not run renders `--`, which the ratio pattern does not // match, so a short list is the signal that this row cannot be summarised. - if (ratios.length !== 3) { + if (ratios.length !== 9) { problems.push( - `${where}: found ${ratios.length} layout ratios, expected 3`, + `${where}: found ${ratios.length / 3} layout ratios, expected 3`, ); continue; } if (ratios.some((r) => r === null)) continue; if (narrowNanos === null) continue; - rows.set(producers, { narrowNanos, ratios }); + rows.set(producers, { narrowNanos, ratios: [ratios[0], ratios[3], ratios[6]] }); } return rows; } @@ -89,7 +109,7 @@ function drainedComparison(lines, path) { const fields = line.trim().split(/\s+/); const producers = finite(fields[0], `${path}: a comparison producer count`); if (producers === null) continue; - const reserving = finite( + const reserving = positive( fields[2], `${path}, drained comparison, ${producers} producers: the reserving_mpsc cost`, ); diff --git a/crates/windows-platform-probes/src/bin/queue_contention/main.rs b/crates/windows-platform-probes/src/bin/queue_contention/main.rs index 4480e0b9e..ded269fae 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/main.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/main.rs @@ -322,11 +322,23 @@ fn render_observation(out: &mut dyn std::fmt::Write, observation: &Observation) ); let _ = writeln!( out, - " the trade -- below 1.00 and the safer claim is also the cheaper" + " the trade. Read the INTERVAL, not the point: an interval entirely" ); let _ = writeln!( out, - " one; above 1.00 and closing the hole costs throughput." + " below 1.00 has the safer claim also the cheaper one, entirely" + ); + let _ = writeln!( + out, + " above it has closing the hole costing throughput, and one that" + ); + let _ = writeln!( + out, + " crosses 1.00 leaves the ordering unsettled however far the point" + ); + let _ = writeln!( + out, + " estimate sits from it -- 0.78x [0.53-1.16] is such a case." ); // Question 3: what does the claim word's apportionment and width cost? From c1d8555a9b1f210012fa16653196c67f04f0cce6 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 21:46:58 -0400 Subject: [PATCH 103/139] docs(queue): name the regime on every same-code control figure `0.68-1.27x` was quoted in four places as "the probe's same-code control" with no regime named. It is the **drained** control. The isolated one is `0.69-1.12x`, and the design note has carried both in a two-row table all along: | regime | same-code control | |---|---| | isolated | median 0.94-1.05x, observed 0.69-1.12x | | drained | median 0.98-1.07x, observed 0.68-1.27x | Three of the four sites sit in isolated context. The README's paragraph is directly under a table headed "In ns per operation, isolated regime", and `D-26`'s note discusses numbers it has just called "the isolated numbers" -- so both hand the reader a control a fifth wider at the top end than the one that applies, which makes a difference look more like dispersion than it is. The fourth site describes a table with columns from both regimes. All four now name the regime. Swept rather than fixed at the reported site: the review named the README, and checking the proposition found two more. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/DESIGN-NOTES.md | 6 ++++-- crates/windows-waitable-queues/README.md | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/windows-waitable-queues/DESIGN-NOTES.md b/crates/windows-waitable-queues/DESIGN-NOTES.md index cb9f7a9f3..94d477814 100644 --- a/crates/windows-waitable-queues/DESIGN-NOTES.md +++ b/crates/windows-waitable-queues/DESIGN-NOTES.md @@ -863,7 +863,8 @@ warm-up; three independent invocations agreed to within noise. **That "within noise" rests on a floor this workspace has since measured to be far wider.** The figure was read against a 2-6% run-to-run spread; seven runs of the same probe later put the same-configuration spread at 7-61% depending on producer -count, and the probe's own same-code control spans 0.68-1.27x. The figures below +count, and the probe's own same-code control spans 0.69-1.12x isolated and +0.68-1.27x drained. The figures below are not retracted -- the direction of `D-26` survived a re-measurement on the shipping type -- but "agreed within noise" is a weaker statement than it reads as, and any difference here smaller than that control should not be treated as @@ -1307,7 +1308,8 @@ three times; the isolated numbers reproduced within noise except one outlier not **Read "within noise" here against the wider floor measured later**: seven runs of this probe put the same-configuration spread at 7-61%, and its same-code -control at 0.68-1.27x, so three agreeing runs establish less than the phrase +control at 0.69-1.12x isolated -- the regime these numbers are in -- against +0.68-1.27x drained, so three agreeing runs establish less than the phrase suggests. See [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 7d0666d47..eb13f1de4 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -453,7 +453,9 @@ this host's 8 physical cores, and the spread is not small at either scale. 226.5 over a 181.5-242.3 range across its five repetitions. The parenthesised ranges in the table above are the wider quantity: the extremes over all fifteen repetitions of the three captured runs. The probe's same-code -control has been measured at 0.68-1.27x over seven runs; see +control for this regime has been measured at 0.69-1.12x over seven runs -- the +drained regime's is wider, at 0.68-1.27x, and does not apply to the isolated +figures above; see [DESIGN-NOTES.md](../windows-platform-probes/DESIGN-NOTES.md#d-variance-is-a-finding). That seven-run sweep is a **separate capture** taken to size the noise floor, not a longer version of this table -- its medians differ from the ones above, which is From b41453b74451c55c0331a3315d4342dcb6a8b801 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 21:56:01 -0400 Subject: [PATCH 104/139] fix(probes): stop the permit column claiming what the harness cannot attribute The `permit/reserving` column's comment said "below 1.00 means the permit claim is cheaper; above means removing the room-decision race costs throughput", and the rendered paragraph said the same in the reader's voice. The queue crate's `D-35` says otherwise about this exact column: "the drained regime's refusal counts differ by orders of magnitude in a way this harness cannot attribute, which SH-15.5.1 exists to settle". So the instrument was asserting a causal reading its own design note records as unavailable -- and the sibling `reserving/slotwise` column already carries the matching caveat, which makes this the same fact stated correctly in one column and incorrectly in the one beside it. Both now say what the ratio is: an end-to-end comparison of two whole push paths, where the shapes differ in refusal and retry behaviour as well as in the claim, so it does not price the room-decision race in either direction. The interval instruction from the previous commit stays; it was necessary and not sufficient, since a ratio clear of 1.00 still orders only the whole paths. **`isolated.js` hard-coded its own sample count.** The footer said "three observations per count" while the script takes any number of runs -- so pointing it at the seven-run sweep would have published a sample count of three. Derived from `files.length`; `isolated.txt` regenerated, and two runs now report two. **The capture README conflated three datasets.** It said the isolated figures "are cited by the queue crate's documentation", which reads as though this directory is where that table came from. It is not: the crate's attributed table is a separate capture from `fecd352`, the `Wide` claim rests on a seven-run sweep whose runs were never committed, and this directory is three runs from `68198359`. It now says which is which, and what this one is for -- an independent cross-check for figures that otherwise have none. **The archived `M4.3` entry named the wrong helper**, appended rather than rewritten because the archive is append-only. It credited `await_consumer` with closing the undrained window; that helper only waits on the flag, and the ordering lives in `drain_then_announce`. It also said "closes", where what a flag can do is narrow the window to a stated guarantee -- one executed pop path, not continuous draining. `await_consumer`'s own doc said so from the start, so the entry was a restatement that had drifted from the thing it restated. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../COMPLETED-CHECKLIST.md | 15 +++++++++++ .../2026-09-16-drained-handshake/README.md | 16 +++++++++--- .../2026-09-16-drained-handshake/isolated.js | 4 +-- .../2026-09-16-drained-handshake/isolated.txt | 4 +-- .../src/bin/queue_contention/main.rs | 25 +++++++++++++------ 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index e6e491983..9ec1fbc44 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1211,6 +1211,21 @@ three control observations, which is not a band. Three runs do not settle the dr either direction. The capture now reports per-count figures and emits no verdict, and the pre-handshake reading rests on the seven-run sweep, which this does not replace. +**Two further corrections, appended because this archive is append-only.** The +paragraph above names `await_consumer` as what closes the window; that helper is +only the *waiting* half -- a producer spinning until the flag is set. The +ordering the item is actually about lives in `drain_then_announce`, which pops +once and only then publishes readiness, and which was extracted into a single +definition later (see the M4.3 review round) precisely because four timers had +been writing it by hand with nothing able to test it. + +And "closes it" overstates what any flag can do. The window is *narrowed to a +stated guarantee*: no producer begins timing until the consumer has executed its +pop path at least once. Continuous draining is not guaranteed and cannot be -- +the consumer can be descheduled immediately afterwards, as it can at any point +during the run. `await_consumer`'s own doc said so from the start, which is what +makes this entry's wording a restatement that drifted from the thing it restated. + ## Moved 2026-09-16 17:31:31 UTC-04:00 -- M2.16: the census that broke the prose around it ### M2.16 -- Repair the garbled `Report` doc comment, and drop the two counts that had rotted beside it. *(completed 2026-09-16 17:31:31 UTC-04:00)* diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md index b498d3f6f..30744f4f3 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md @@ -58,10 +58,18 @@ node isolated.js run1.txt run2.txt run3.txt ``` The two cover different regimes and are kept apart for that reason: `summarise.js` -derives the **drained** tables, `isolated.js` the **isolated** ones. The isolated -figures are cited by the queue crate's own documentation -- which says the whole -push path was measured as slower under `Wide` at every producer count -- so they -need a derivation a reader can run rather than a number taken on trust. +derives the **drained** tables, `isolated.js` the **isolated** ones. + +The isolated figures matter beyond this directory because the queue crate's +documentation makes a claim about them -- that the whole push path was measured +as slower under `Wide` at every producer count. That claim rests on a **separate** +seven-run sweep whose raw runs were never committed, and on the crate's own +attributed table, which is a **third** capture built from `fecd352`. This +directory is neither of those: it is three runs from `68198359`, and what +`isolated.js` provides is an independent cross-check that can actually be run, +against figures that otherwise have none. Where this and the crate's table +disagree, the crate's table is the attributed figure for that crate; this one is +evidence about how far such a figure moves. Both are committed so the derivation can be checked rather than taken on trust, and so nothing downstream has to retype a figure. The scripts derive what the runs do diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index c189b3090..c90adf9fd 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -131,7 +131,7 @@ for (const l of LAYOUTS) { } console.log( - "\nThe control's range here is three observations per count, which is not a\n" + - "band. This reports what these runs did; it does not establish that a fresh\n" + + `\nThe control's range here is ${files.length} observation(s) per count, which is not\n` + + "a band. This reports what these runs did; it does not establish that a fresh\n" + "run would land the same way.", ); diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt index 86d55bc47..6167d4906 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt @@ -15,6 +15,6 @@ where every run sat above the control's whole observed range: reserving(8/56) 32 producers reserving(64/64) 1, 2, 4, 8, 16, 32 producers -The control's range here is three observations per count, which is not a -band. This reports what these runs did; it does not establish that a fresh +The control's range here is 3 observation(s) per count, which is not +a band. This reports what these runs did; it does not establish that a fresh run would land the same way. diff --git a/crates/windows-platform-probes/src/bin/queue_contention/main.rs b/crates/windows-platform-probes/src/bin/queue_contention/main.rs index ded269fae..01d352823 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/main.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/main.rs @@ -246,9 +246,13 @@ fn render_observation(out: &mut dyn std::fmt::Write, observation: &Observation) format_ratio_bounded(reserving, plain), format_nanos(permit), // The column SH-15.5 exists to fill: the experimental claim - // against the shipping shape it would replace. Below 1.00 means - // the permit claim is cheaper; above means removing the - // room-decision race costs throughput. + // against the shipping shape it would replace. It is an + // end-to-end ratio of two whole push paths, not a price on the + // room-decision race: the shapes also differ in refusal and + // retry behaviour, and the queue crate's D-35 records that in + // this regime their refusal counts differ by orders of + // magnitude the harness cannot attribute -- which is what + // SH-15.5.1 exists to settle. format_ratio_bounded(permit, reserving), ) }) @@ -322,24 +326,29 @@ fn render_observation(out: &mut dyn std::fmt::Write, observation: &Observation) ); let _ = writeln!( out, - " the trade. Read the INTERVAL, not the point: an interval entirely" + " the trade. Read the INTERVAL, not the point: one that crosses" ); let _ = writeln!( out, - " below 1.00 has the safer claim also the cheaper one, entirely" + " 1.00 orders nothing, however far the point estimate sits from" ); let _ = writeln!( out, - " above it has closing the hole costing throughput, and one that" + " it -- 0.78x [0.53-1.16] is such a case. An interval clear of" ); let _ = writeln!( out, - " crosses 1.00 leaves the ordering unsettled however far the point" + " 1.00 orders the two WHOLE PUSH PATHS and not the room-decision" ); let _ = writeln!( out, - " estimate sits from it -- 0.78x [0.53-1.16] is such a case." + " race on its own: the shapes differ in refusal and retry" ); + let _ = writeln!( + out, + " behaviour too, and here their refusal counts differ by orders" + ); + let _ = writeln!(out, " of magnitude this harness cannot attribute."); // Question 3: what does the claim word's apportionment and width cost? let _ = writeln!(out, "\n 3. claim-word layout\n"); From f466de9592cc82365540d34e824c007815d1f244 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 21:56:01 -0400 Subject: [PATCH 105/139] docs: make the artifact rule's own example obey the rule beside it Rule 4's illustration of a claim that cannot drift was "the spread is wide enough that the ordering is a flag rather than a finding" -- and rule 5, directly below it, classifies exactly that as a conclusion drawn on the reader's behalf. The example meant to show good practice violated the next rule on the page, in both of its homes. It now reads "over a spread that overlaps the same-code control at every producer count": an observation, still free of digits, and still unable to drift. The same section asserted that "a figure behind a link is a figure most readers will not look at". That is an unmeasured claim about readers, made in a passage arguing against unmeasured claims. The trade is now stated as what it is -- a figure in the prose is read by whoever reads the sentence, a figure behind a link by whoever follows it, which is a different and unmeasured set -- without asserting which is larger. Reported by review, which is the third time this round that a rule on this branch was found breaking itself in its own example. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 2 +- DESIGN-NOTES.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1acc39d36..7f1527efe 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1285,7 +1285,7 @@ one file away, an attribution naming a superseded capture, one horizon left unqu sites in three wordings. - **Write the claim, not the digits, wherever the digits are not the point.** "Measured faster under - contention, and the spread is wide enough that the ordering is a flag rather than a finding" cannot + contention, over a spread that overlaps the same-code control at every producer count" cannot drift from the data, because it restates none of it. - **When a figure must appear, it has exactly one home.** Prefer a committed capture the prose links to (`mutation-sweeps//` is this repository's existing example) over the same figure typed diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 33754a777..09b5d28a3 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1997,7 +1997,7 @@ narrowed rather than closed. So the principle, which holds regardless of which mechanism is eventually chosen: - **A claim belongs in prose.** "`reserving_mpsc` measured faster than `slotwise_mpsc` under - contention, and the spread is wide enough that the ordering is a flag rather than a finding" is a + contention, over a spread that overlaps the same-code control at every producer count" is a claim. It contains no digits, so it cannot drift from the data -- it can only be wrong about it, which a reader can see. - **A number belongs in an artifact.** A measured cost, a capture's commit, a count of occurrences: @@ -2011,7 +2011,8 @@ If this were adopted, the "which restatements are mechanically checkable" questi note **dissolves** rather than being answered: all of them, because none would be restated. **The mechanism is undecided and no work is scheduled here.** The reader-experience trade is real -- -a figure behind a link is a figure most readers will not look at -- and it has not been settled. +a figure in the prose is read by whoever reads the sentence, and a figure behind a link is read by +whoever follows it, which is a different and unmeasured set -- and it has not been settled. Recorded as a principle so the next person choosing where to paste a number has the argument in front of them, not as a queued change. Per "design notes are not a work queue", the absence of a checklist item is deliberate. From 7142690909e406f464090486bbb8e819366861e4 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:05:46 -0400 Subject: [PATCH 106/139] fix(probes): stop baking a stale figure into every fresh report **The report printed a measurement from a different capture.** Two rounds ago I added `0.78x [0.53-1.16]` to the paragraph under the comparison table as a worked example of an interval that crosses 1.00. It is a literal, so every fresh run printed it beside its own `permit/reserving` row regardless of what that row said -- historical data presented as the current observation, which is exactly the stale-footer defect `M4.7` existed to remove. The sentence is now number-free. Swept the class: the remaining literals in the renderer are `1.00`, which is a ratio scale's definition point rather than a measurement, so they stay. **A rustdoc link had been one directory too shallow since the file moved.** `src/bin/queue_contention.rs` became `src/bin/queue_contention/main.rs`, adding a level, and `../../DESIGN-NOTES.md` was not updated -- it resolves to `src/DESIGN-NOTES.md`, which does not exist. Checked every relative link in that directory rather than only the reported one; it was the only casualty. **`summarise.js` summarised an empty argument list.** With no captures, every completeness check iterates nothing and finds nothing to complain about, `median([])` is NaN, and the script printed an empty summary and exited 0. It now rejects an empty list and exits 2, as `isolated.js` already did. **`DESIGN-RATIONALE.md` gave the opposite execution order to the checklist.** It records lengthening the run as the first diagnostic step because it "changes nothing about what is being measured", while `CHECKLIST.md` deliberately puts `M4.4` before `M4.2` and says why. The checklist is right and the rationale's argument has a false premise: it holds only if the control is comparable to the candidate, and `measure()` runs the control roughly four configurations away, so a lengthened unpaired control is still confounded by whatever drifts across that distance. Amended in place, with the original kept as what was current when the diagnosis was first written. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 3 ++- crates/windows-platform-probes/DESIGN-RATIONALE.md | 11 +++++++++++ .../2026-09-16-drained-handshake/summarise.js | 8 ++++++++ .../src/bin/queue_contention/main.rs | 7 ++----- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7f1527efe..f7d719cdb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1288,7 +1288,8 @@ sites in three wordings. contention, over a spread that overlaps the same-code control at every producer count" cannot drift from the data, because it restates none of it. - **When a figure must appear, it has exactly one home.** Prefer a committed capture the prose links - to (`mutation-sweeps//` is this repository's existing example) over the same figure typed + to ([mutation-sweeps/2026-09-02/](../mutation-sweeps/2026-09-02) is this repository's existing + example) over the same figure typed into two documents. Provenance — host, commit, date — travels with the data rather than in a hand-maintained table beside it. - **Never restate a proportion over data you already showed.** A ratio over counts in the same diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 7c919e61d..386be7098 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -623,6 +623,17 @@ probe all move the measurement as well as the noise, and a change made ahead of the cheap check cannot be evaluated against anything. That this also happens to be the least effortful step is a convenience, not the reason. +**That ordering has since been revised, and the revision is in +[CHECKLIST.md](CHECKLIST.md): `M4.4` comes before `M4.2`.** The argument above +holds only if the control is comparable to the candidate, and in this probe it is +not: `measure()` runs the control roughly four configurations away from the row +it is a control for, so a lengthened run still carries whatever drifts across +that distance. Lengthening an unpaired control buys a narrower interval around a +quantity that is still confounded by sequence, which is not an interpretable +result -- so interleaving the control with its candidate has to land first. The +paragraph above is kept as the reasoning that was current when the diagnosis was +first written down; where the two disagree, the checklist is the execution order. + The other half is knowing when to stop. Every setup has a floor, and past it more runs buy nothing; the failure mode is a week spent establishing that two numbers are the same. What makes the floor easy to misjudge is the assumption that it diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 726dab896..209f14613 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -120,6 +120,14 @@ function drainedComparison(lines, path) { } const paths = process.argv.slice(2); +// An empty argument list summarises nothing: every completeness check below +// iterates the captures, so with none of them there is nothing to complain +// about, `median([])` is NaN, and the script prints an empty summary and exits +// successfully. Rejected here, as `isolated.js` does. +if (paths.length === 0) { + console.error("usage: node summarise.js [run.txt ...]"); + process.exit(2); +} const layouts = []; const controls = []; for (const path of paths) { diff --git a/crates/windows-platform-probes/src/bin/queue_contention/main.rs b/crates/windows-platform-probes/src/bin/queue_contention/main.rs index 01d352823..a34602898 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/main.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/main.rs @@ -5,7 +5,7 @@ //! **An experiment, not a component.** These probes measure platform behaviour //! and are not for production use. Do not call them from production code, and //! do not lift a technique out of here. See this crate's -//! [DESIGN-NOTES.md](../../DESIGN-NOTES.md). +//! [DESIGN-NOTES.md](../../../DESIGN-NOTES.md). //! //! This reports observations that bear on two questions otherwise settled by //! taste: whether the linked and sharded MPSC shapes are ever needed, and @@ -332,10 +332,7 @@ fn render_observation(out: &mut dyn std::fmt::Write, observation: &Observation) out, " 1.00 orders nothing, however far the point estimate sits from" ); - let _ = writeln!( - out, - " it -- 0.78x [0.53-1.16] is such a case. An interval clear of" - ); + let _ = writeln!(out, " it. An interval clear of"); let _ = writeln!( out, " 1.00 orders the two WHOLE PUSH PATHS and not the room-decision" From 588f56d60aa0c044b2f17c47ae25a4cb84a66aee Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:05:46 -0400 Subject: [PATCH 107/139] test(queue): bound the reservation-exhaustion loop so a regression cannot OOM `one_producer_alone_can_exhaust_the_reservation_field` collected from an unbounded `repeat_with(|| tx.reserve())`, stopping only when `reserve` returned `None`. A guard that regressed to never refusing would therefore allocate until the process died -- and this suite runs its tests as threads in one process, so that takes every other test with it rather than reporting one failure. Now capped at `MAX_RESERVED + 1` attempts, matching `reservations_stop_at_the_layouts_ceiling`. The assertion that follows is unchanged and still catches the regression, reporting a count one too high instead of the harness disappearing. **Recorded honestly: I could not construct a faithful regression for this.** Replacing the refusal guard with `if false` does not reach the runaway-allocation path -- for `Perpetual` the count field is eight bits, so never refusing corrupts the packed claim word long before any loop could exhaust memory, and the test process aborts with `STATUS_STACK_BUFFER_OVERRUN` instead. The bound is therefore structural insurance whose triggering condition I have not demonstrated, rather than a guard shown red-then-green. It costs nothing and the reasoning is sound, but it has not been verified the way the other changes on this branch were. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/src/reserving_mpsc/tests.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/windows-waitable-queues/src/reserving_mpsc/tests.rs b/crates/windows-waitable-queues/src/reserving_mpsc/tests.rs index 667d56754..9d2ee5cc5 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc/tests.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc/tests.rs @@ -1780,7 +1780,13 @@ fn one_producer_alone_can_exhaust_the_reservation_field() { // Perpetual is 8/56: the count field holds at most 255. let (tx, _rx) = bounded_as::(1024).expect("a valid capacity"); + // Bounded deliberately. Were the guard to regress to never refusing, an + // unbounded `repeat_with` would allocate until the process died -- and this + // suite runs its tests as threads in one process, so that takes every other + // test with it. One attempt past the ceiling is enough: the assertion below + // then reports a count one too high instead of the harness disappearing. let held: Vec<_> = std::iter::repeat_with(|| tx.reserve()) + .take(Perpetual::MAX_RESERVED as usize + 1) .take_while(Option::is_some) .flatten() .collect(); From 5d441949c9884ecc4108d21e3e591ea9dbffb737 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:20:29 -0400 Subject: [PATCH 108/139] fix(probes): unlink a public field's doc from a private item CI's `rustdoc (intra-doc links)` job has been failing since `96f5059b`. The `drained` field is public and its doc linked `[`await_consumer`]`, which is private, so rustdoc emits `private_intra_doc_links` and that job denies warnings. The name stays as a plain code span: it is the right thing to point a reader at, and a link that resolves only under `--document-private-items` is not. **It was missed because I verified the wrong crate.** After the docs changes in that round I ran `cargo doc -p windows-waitable-queues --all-features` and read "clean" as covering the branch, when the edit was in `windows-platform-probes`. Locally the lint is a *warning*, so nothing failed here even when the right crate was built -- `cargo check`, `cargo clippy` and the test suite are all silent on it, and only CI's deny turns it red. Verified the way CI does rather than the way that missed it: `cargo doc` across the default workspace with `RUSTDOCFLAGS=-D warnings`, with and without `--all-features`, both clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/src/queue_contention.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 787c25bfc..87a25d110 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -262,7 +262,7 @@ pub struct Observation { pub isolated: Vec, /// Producers timed against a consumer looping on `pop`. /// - /// The handshake in [`await_consumer`] guarantees that loop has executed at + /// The handshake in `await_consumer` guarantees that loop has executed at /// least once before any producer starts timing. It does not guarantee the /// consumer is never descheduled afterwards, so "looping" describes what the /// consumer thread runs, not how continuously it is scheduled to run it. From 0f6248d0ecadef0a7f00745d621cb8828637c616 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:26:33 -0400 Subject: [PATCH 109/139] fix(probes): route the capture scripts through one writer Twenty-five direct `console.log`/`console.error` calls across the two capture tools, against this repository's standing rule that a tool must not write to stdout from more than one site. Every formatting site now hands text to `out`, and the diagnostic path to `fail` -- two sinks rather than one with a flag, because the artifact and the complaint about the artifact do not share a destination. Retargeting either is a change in one place. Both committed outputs still reproduce byte-identically, and both scripts still exit 2 on an empty argument list. Also: a comment that began "Through `positive` like every other captured value" was a fragment and is now imperative, and the archived `M4.7` entry's references to `main.rs`, `tests.rs` and `corpus.json` are clickable links rather than bare inline code. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../COMPLETED-CHECKLIST.md | 6 ++- .../2026-09-16-drained-handshake/isolated.js | 26 ++++++---- .../2026-09-16-drained-handshake/summarise.js | 48 +++++++++++-------- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 9ec1fbc44..b603d0b7e 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1275,10 +1275,12 @@ result beneath a table produced by one invocation. `render` now writes the banner and calls `render_observation(out, &measure())`. The banner stays outside because it is a fresh topology read rather than a function of the observation, which is what keeps the rendering half pure and therefore drivable by a fixture. The binary moved to -`src/bin/queue_contention/main.rs` so it can carry a sibling `tests.rs`, following +[src/bin/queue_contention/main.rs](src/bin/queue_contention/main.rs) so it can carry a sibling +[tests.rs](src/bin/queue_contention/tests.rs), following `windows-placement-probe`'s layout; git recorded it as a rename, so history follows. -**The cases are data, not code.** `corpus.json` holds an observation and what the rendered report +**The cases are data, not code.** [corpus.json](src/bin/queue_contention/corpus.json) holds an +observation and what the rendered report must be true of, so adding a case needs no Rust. The central check is *derived rather than restated*: `aligned_tables` asserts every line of a named table is the same length, which is exactly the property a cell wider than its column breaks. It therefore catches width bugs the corpus never diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index c90adf9fd..7262b2087 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -22,9 +22,17 @@ const fs = require("fs"); +// One writer for the generated artifact, per this repository's output rule: no +// formatting site picks a destination, so retargeting the report to a file is a +// change here and nowhere else. ail is the diagnostic path and stays separate +// from the artifact, which is why they are two sinks rather than one with a flag. +let sink = (text) => process.stdout.write(text + "\n"); +const out = (text = "") => sink(text); +const fail = (text) => process.stderr.write(text + "\n"); + const files = process.argv.slice(2); if (files.length === 0) { - console.error("usage: node isolated.js [run.txt ...]"); + fail("usage: node isolated.js [run.txt ...]"); process.exit(2); } @@ -105,15 +113,15 @@ const width = COLUMNS.map((name, column) => ); const PRODUCERS_WIDTH = Math.max(9, ...COUNTS.map((n) => String(n).length)); -console.log(`isolated regime, ${files.length} run(s): ${files.join(", ")}`); -console.log(`each layout against ${DEFAULT_LAYOUT}; control is ${DEFAULT_LAYOUT} against ${CONTROL_TWIN}`); -console.log("median of the per-run ratios, with the observed range beside it\n"); +out(`isolated regime, ${files.length} run(s): ${files.join(", ")}`); +out(`each layout against ${DEFAULT_LAYOUT}; control is ${DEFAULT_LAYOUT} against ${CONTROL_TWIN}`); +out("median of the per-run ratios, with the observed range beside it\n"); -console.log( +out( ["producers".padEnd(PRODUCERS_WIDTH + 2), ...COLUMNS.map((name, i) => name.padEnd(width[i] + 2))].join(""), ); COUNTS.forEach((n, row) => { - console.log( + out( [ String(n).padEnd(PRODUCERS_WIDTH + 2), ...body[row].map((c, i) => c.padEnd(width[i] + 2)), @@ -121,16 +129,16 @@ COUNTS.forEach((n, row) => { ); }); -console.log("\nwhere every run sat above the control's whole observed range:"); +out("\nwhere every run sat above the control's whole observed range:"); for (const l of LAYOUTS) { const above = COUNTS.filter((n) => { const top = Math.max(...control.get(n)); return measured.get(l).get(n).every((x) => x > top); }); - console.log(` ${l.padEnd(18)} ${above.length ? above.join(", ") + " producers" : "no producer count"}`); + out(` ${l.padEnd(18)} ${above.length ? above.join(", ") + " producers" : "no producer count"}`); } -console.log( +out( `\nThe control's range here is ${files.length} observation(s) per count, which is not\n` + "a band. This reports what these runs did; it does not establish that a fresh\n" + "run would land the same way.", diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 209f14613..e93709d17 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -8,6 +8,14 @@ const fs = require("fs"); +// One writer for the generated artifact, per this repository's output rule: no +// formatting site picks a destination, so retargeting the report to a file is a +// change here and nowhere else. ail is the diagnostic path and stays separate +// from the artifact, which is why they are two sinks rather than one with a flag. +let sink = (text) => process.stdout.write(text + "\n"); +const out = (text = "") => sink(text); +const fail = (text) => process.stderr.write(text + "\n"); + // A ratio cell, with all three numbers required to be plain decimals. The // looser `[0-9.]+` also matched a run of dots, so a malformed `...x [..-..]` // parsed and `Number("...")` became NaN -- which compares false against every @@ -77,10 +85,10 @@ function drainedLayout(lines, path) { if (producers === null) continue; const where = `${path}, drained layout, ${producers} producers`; const narrowNanos = positive(fields[1], `${where}: the 32/32 cost`); - // Through `positive` like every other captured value, and all three numbers - // of each cell are checked -- the two bounds are not used by this script, - // but a capture carrying an unreadable bound is not a capture this script - // should certify as summarised. + // Pass every value through `positive`, like every other captured quantity, + // and check all three numbers of each cell -- the two bounds are not used by + // this script, but a capture carrying an unreadable bound is not a capture + // this script should certify as summarised. const ratios = [...line.matchAll(RATIO)].flatMap((m, i) => [ positive(m[1], `${where}: layout ratio ${i + 1}`), positive(m[2], `${where}: layout ratio ${i + 1} lower bound`), @@ -125,7 +133,7 @@ const paths = process.argv.slice(2); // about, `median([])` is NaN, and the script prints an empty summary and exits // successfully. Rejected here, as `isolated.js` does. if (paths.length === 0) { - console.error("usage: node summarise.js [run.txt ...]"); + fail("usage: node summarise.js [run.txt ...]"); process.exit(2); } const layouts = []; @@ -169,7 +177,7 @@ layouts.forEach((layout, i) => { }); const producers = EXPECTED_PRODUCERS; -console.log(`runs: ${paths.length}`); +out(`runs: ${paths.length}`); // **Reported per producer count, and deliberately without a verdict.** // @@ -215,20 +223,20 @@ for (const p of producers) { // comparison against `NaN` is false -- so an unreadable cell used to empty the // "outside the band" list and print `true`. if (problems.length > 0 || rows.length === 0) { - console.log(""); - console.log("CAPTURE INCOMPLETE -- not summarised:"); - if (rows.length === 0) console.log(" - no complete producer counts were read"); - for (const problem of problems) console.log(` - ${problem}`); + out(""); + out("CAPTURE INCOMPLETE -- not summarised:"); + if (rows.length === 0) out(" - no complete producer counts were read"); + for (const problem of problems) out(` - ${problem}`); process.exitCode = 1; return; } -console.log(""); -console.log( +out(""); +out( "drained, per producer count: the same-code control's observed range, then", ); -console.log("each layout's median ratio against 32/32, across runs."); -console.log(""); +out("each layout's median ratio against 32/32, across runs."); +out(""); // Derived, with the table's original width as a floor: a control band is built // from measured ratios and has no upper bound, so a fixed field would shift the // layout columns the first time one outgrew it. @@ -238,21 +246,21 @@ const bands = rows.map((row) => { return `${low.toFixed(2)}-${high.toFixed(2)}(${row.control.length})`; }); const bandWidth = Math.max(16, "control(n)".length, ...bands.map((b) => b.length)); -console.log(`producers ${"control(n)".padEnd(bandWidth)} 16/48 8/56 64/64`); +out(`producers ${"control(n)".padEnd(bandWidth)} 16/48 8/56 64/64`); rows.forEach((row, i) => { - console.log( + out( `${String(row.producers).padStart(9)} ${bands[i].padEnd(bandWidth)} ` + row.medians.map((m) => `${m.toFixed(2)}x`).join(" "), ); }); const everyControl = rows.flatMap((row) => row.control); -console.log(""); -console.log( +out(""); +out( `control observations: ${everyControl.length} across ${rows.length} producer counts, ` + `${Math.min(...everyControl).toFixed(2)}x to ${Math.max(...everyControl).toFixed(2)}x pooled`, ); -console.log( +out( "Pooled only to show the spread; it is not a band to judge a median against,", ); -console.log("for the reason recorded in this script beside the table above."); +out("for the reason recorded in this script beside the table above."); From 16465fb20a888135851d308da90ebfea34082b7c Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:26:33 -0400 Subject: [PATCH 110/139] ci: document windows-waitable-queues in its default-feature job The `docs` job documents `--all-features` only, so nothing checked the queue crate's rustdoc in the configuration most consumers build. The placement-probe job already carries the matching step for exactly this reason; this one stopped after build, clippy and test. The gap is not hypothetical. `rustdoc (intra-doc links)` failed on this branch for nine commits because a public field's doc linked a private item, and it went unnoticed locally because `private_intra_doc_links` is a *warning* by default -- `cargo check`, `cargo clippy` and the test suite are all silent on it. Only a deny catches it, and only a job that runs `cargo doc` for this crate would have. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6687e07d6..a421ec082 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -538,6 +538,15 @@ jobs: RUST_BACKTRACE: 1 RUST_LIB_BACKTRACE: 1 run: cargo test -p windows-waitable-queues --locked --no-fail-fast + # The `docs` job documents `--all-features` only, so a link into a + # `dwcas`-gated item resolves there and dangles in this configuration. + # `private_intra_doc_links` is denied here for the same reason it is in the + # placement-probe job: locally it is only a warning, so nothing but a deny + # catches a public item documented against a private one. + - name: cargo doc (deny broken intra-doc links) + env: + RUSTDOCFLAGS: "-D rustdoc::broken_intra_doc_links -D rustdoc::private_intra_doc_links" + run: cargo doc -p windows-waitable-queues --no-deps --locked fmt: name: rustfmt From d6104b396ffea7dea00c0667e21c165a4966b22d Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:26:33 -0400 Subject: [PATCH 111/139] docs(queue): the field ceiling is reachable, and this branch proves it Both the crate doc and the README opened the reservation-count explanation with "the field's ceiling, not a reachable number of reservations" -- and the rest of the same paragraph then says the achievable count is the lesser of capacity and the field, which is a different and correct claim. The opening is contradicted by a test added on this branch: `one_producer_alone_can_exhaust_the_reservation_field` fills `Perpetual`'s 255 from a single producer on a ring of 1024. The ceiling is reachable wherever capacity allows; what it is not is a count every queue reaches. That matters beyond wording, because the false reading is the one that makes the narrow fields look harmless -- which is the argument for spending bits on the position, and the same mistake D-41 already records withdrawing once. Both sites corrected, and both now name the test that settles it. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/README.md | 11 +++++++---- crates/windows-waitable-queues/src/lib.rs | 9 ++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index eb13f1de4..5ed563de4 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -140,10 +140,13 @@ floor, saying the wrap arrives sooner than it does, which is the conservative direction for a hazard. The horizon that matters is the one on your hardware at your rate. -The middle column is the field's ceiling, not a reachable number of reservations: -admission is also bounded by capacity -- `reserve` refuses once the ring has no -room beyond the reservations already outstanding -- so the achievable count is -the lesser of the two. For `Balanced` the capacity bound binds first, since that +The middle column is the field's ceiling rather than the count any particular +queue reaches: admission is also bounded by capacity -- `reserve` refuses once +the ring has no room beyond the reservations already outstanding -- so the +achievable count is the lesser of the two. It is reachable where capacity allows: +one producer alone fills `Perpetual`'s 255 in a loop given a ring that large, +which `one_producer_alone_can_exhaust_the_reservation_field` pins. For `Balanced` +the capacity bound binds first, since that layout accepts at most 2^31 slots on a 64-bit target, and 2^30 on a 32-bit one. For the others the field is the smaller number only once the queue is at least that large: a `Perpetual` queue of capacity 64 can hold 64 reservations, not 255. The achievable count is always the lesser of the two. diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index ea3cd703d..9967b79de 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -109,9 +109,12 @@ //! than it does, which is the conservative direction for a hazard. The horizon //! that matters is the one on your hardware at your rate. //! -//! The reservation-count column is the field's ceiling, not a reachable number of -//! reservations: admission is also bounded by capacity, so the achievable count -//! is the lesser of the two. For `Balanced` the capacity bound binds first -- +//! The reservation-count column is the field's ceiling rather than the count any +//! particular queue reaches: admission is also bounded by capacity, so the +//! achievable count is the lesser of the two. It is reachable where capacity +//! allows -- one producer alone fills `Perpetual`'s 255 in a loop given a ring +//! that large, which `one_producer_alone_can_exhaust_the_reservation_field` +//! pins. For `Balanced` the capacity bound binds first -- //! that layout accepts at most 2^31 slots on a 64-bit target, and 2^30 on a //! 32-bit one. For the others the field is the smaller number only once //! the queue is at least that large: a `Perpetual` queue of capacity 64 can From 8b3b4df5428c31705a4dffdb27686a404f2d4d3e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:26:33 -0400 Subject: [PATCH 112/139] docs: stop naming an owner that does not exist for the loom verification The convergence note said the `loom` verification was owned by `M31.6`. Five documents name that item -- `D-9`, `D-31`, `doorbell.rs`, `sabotage.json`, and two more passages in the queue crate's design notes -- and **no CHECKLIST contains it**. `windows-waitable-queues` has no CHECKLIST.md at all, only a COMPLETED-CHECKLIST.md. So the work is unscheduled, and six documents were describing it as tracked. That is precisely the failure "design notes are not a work queue" exists to prevent: an obligation recorded only in prose that nothing will ever cause to be picked up. My sentence made it six; it now says the verification is unscheduled and says why, so the gap is visible rather than papered over by a plausible ID. Naming the gap is the limit of what belongs in this pull request. Creating the item means deciding the shape of another crate's plan, which is the engineer's call, not a side effect of a review round. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 09b5d28a3..ae2cf7c90 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1944,8 +1944,10 @@ Three conclusions, of which the middle one is the one that changes practice. **Formal specification and prose reduction address different classes.** TLA+ and `loom` ([D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31)) target algorithm properties. Neither -has been run -- D-31 records the `loom` verification as planned, and `M31.6` still owns it -- so -what can be said about that class is that it produced no findings in any review round of PR #90 +has been run, and neither is scheduled: `D-31` records the `loom` verification as planned, and five +documents name `M31.6` as its owner, but no CHECKLIST contains that item -- `windows-waitable-queues` +has no CHECKLIST.md at all, only a COMPLETED-CHECKLIST.md. So what can be said about that class is +that it produced no findings in any review round of PR #90 while carrying one known unfound defect, which is a statement about the reviews rather than a result from either instrument. Restatement targets documented facts, which have produced most findings. Both are worth doing; conflating them would aim the expensive From 60fc576f79c33375771d79497447edef21d9ba17 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:41:50 -0400 Subject: [PATCH 113/139] fix(probes): reject duplicate capture rows and unordered intervals Three ways a malformed capture could be summarised as a valid one. **A `Map` keeps the last write.** Both parsers in `summarise.js` and the one in `isolated.js` inserted by key without checking, so a run carrying the whole expected producer set *plus* a duplicate row passed every completeness check -- those compare distinct keys -- while one measurement was silently discarded in favour of another. It matters most in the comparison table, whose value is the same-code control: quietly preferring the later of two duplicates moves the published control range. **An interval was never checked for being one.** `positive` accepts each of a cell's three numbers on its own, so `2.00x [3.00-1.00]` passed three separate checks. `orderedTriple` now requires `low <= point <= high` before the triple is accepted. Verified by sabotage, each against the committed runs: an inverted bound reports `bound 3 to 1 is inverted`, a duplicated layout row reports `a second layout row for this producer count`, and a duplicated isolated row throws naming the key. All three were accepted silently before. Both committed outputs still reproduce byte-identically. **A rustdoc link that could never resolve.** `drain_then_announce`'s doc pointed at `crate::queue_contention::tests`, which exists only under `#[cfg(test)]`. It does not fail CI -- no job passes `--document-private-items`, and this is a private item, verified by running the workspace `cargo doc` with CI's exact deny flags -- but a link that resolves in no ordinary build should not be a link. The test is named in plain text instead. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/isolated.js | 11 +++++- .../2026-09-16-drained-handshake/summarise.js | 37 +++++++++++++++++++ .../src/queue_contention.rs | 6 +-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 7262b2087..1c4425ff0 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -54,7 +54,16 @@ function isolatedRows(file) { const rows = new Map(); for (const line of lines.slice(start, end)) { const m = line.match(/^(\S+)\s+(\d+)\s+([\d.]+)\s/); - if (m) rows.set(`${m[1]}@${m[2]}`, Number(m[3])); + if (m) { + const key = `${m[1]}@${m[2]}`; + // A `Map` keeps the last write. A run carrying the whole expected producer + // set plus one duplicated row would pass every completeness check while + // one measurement was silently discarded in favour of another. + if (rows.has(key)) { + throw new Error(`${file}: a second isolated row for ${key}`); + } + rows.set(key, Number(m[3])); + } } return rows; } diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index e93709d17..257c06569 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -64,6 +64,22 @@ function positive(text, what) { return value; } +// A ratio triple must be ordered and must contain its own point estimate. +// `positive` accepts each number on its own, so `2.00x [3.00-1.00]` passes +// three separate checks and is still not a interval any instrument produced. +function orderedTriple(point, low, high, where) { + if (point === null || low === null || high === null) return false; + if (low > high) { + problems.push(`${where}: bound ${low} to ${high} is inverted`); + return false; + } + if (point < low || point > high) { + problems.push(`${where}: median ${point} is outside its own bound ${low}-${high}`); + return false; + } + return true; +} + // producers -> { narrowNanos, ratios: [16/48, 8/56, 64/64] } function drainedLayout(lines, path) { let start = -1; @@ -104,6 +120,18 @@ function drainedLayout(lines, path) { } if (ratios.some((r) => r === null)) continue; if (narrowNanos === null) continue; + // Each triple must be an interval containing its own median. + const ordered = [0, 3, 6].every((i) => + orderedTriple(ratios[i], ratios[i + 1], ratios[i + 2], `${where}: layout ratio ${i / 3 + 1}`), + ); + if (!ordered) continue; + // A `Map` keeps the last write, so a duplicated producer row would silently + // discard the earlier measurement while the completeness check -- which sees + // only distinct keys -- still reported a whole capture. + if (rows.has(producers)) { + problems.push(`${where}: a second layout row for this producer count`); + continue; + } rows.set(producers, { narrowNanos, ratios: [ratios[0], ratios[3], ratios[6]] }); } return rows; @@ -122,6 +150,15 @@ function drainedComparison(lines, path) { `${path}, drained comparison, ${producers} producers: the reserving_mpsc cost`, ); if (reserving === null) continue; + // Same overwrite hazard as the layout table, and it matters more here: this + // value is the same-code control, so silently keeping the last of two + // duplicates would move the published control range. + if (rows.has(producers)) { + problems.push( + `${path}, drained comparison, ${producers} producers: a second row for this producer count`, + ); + continue; + } rows.set(producers, reserving); } return rows; diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 87a25d110..0d61f17b4 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -1209,8 +1209,8 @@ fn await_consumer(ready: &AtomicBool) { /// hand-written `pop` followed by a `store` in each of them is four chances for /// the two lines to end up the other way round, which no test could see: the /// timers cannot run without running the whole probe. Defining it here gives the -/// ordering a single home that [`the_handshake_drains_before_it_announces`] can -/// drive with a recording fake. +/// ordering a single home that the `the_handshake_drains_before_it_announces` +/// test can drive with a recording fake. /// /// Swapping these two statements reintroduces the undrained opening in its /// narrower form -- the announcement would mean "this consumer is about to @@ -1219,8 +1219,6 @@ fn await_consumer(ready: &AtomicBool) { /// /// `Release` pairs with the `Acquire` in [`await_consumer`], so a producer that /// observes the flag has the pop ordered before it. -/// -/// [`the_handshake_drains_before_it_announces`]: crate::queue_contention::tests fn drain_then_announce(pop_once: impl FnOnce(), ready: &AtomicBool) { pop_once(); ready.store(true, Ordering::Release); From 2e8310b06b996df64245f7098533f0fc7362b924 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:41:50 -0400 Subject: [PATCH 114/139] docs: dropping the digits removes transcription drift, not the duty to cite The rule illustrated a claim that "cannot drift from the data, because it restates none of it". That overstates it, and the overstatement is the kind the section exists to catch: a digit-free sentence cannot suffer *transcription* drift, because it transcribes nothing, but a retake can still make it false -- and a reader cannot tell from the sentence alone. Omitting digits removes one failure and leaves the citation obligation exactly where it was, which both homes of the rule now say. **Also adds the Tier 2 entry the convention requires.** The decision was recorded in [DESIGN-NOTES.md](DESIGN-NOTES.md) without a matching [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md) section, so the reasoning -- the review history it came from, and the two weaker rules rejected on the way ("keep the copies in sync", which is what was already failing, and "never publish a figure", which trades one failure for another) -- existed nowhere. The correction above is recorded there too, since a rationale that omitted it would preserve the overstated form. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 6 ++++-- DESIGN-NOTES.md | 6 ++++-- DESIGN-RATIONALE.md | 28 ++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f7d719cdb..d7735f317 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1285,8 +1285,10 @@ one file away, an attribution naming a superseded capture, one horizon left unqu sites in three wordings. - **Write the claim, not the digits, wherever the digits are not the point.** "Measured faster under - contention, over a spread that overlaps the same-code control at every producer count" cannot - drift from the data, because it restates none of it. + contention, over a spread that overlaps the same-code control at every producer count" carries no + transcribed figure, so it cannot drift *from* the artifact the way a pasted number does. It can + still be made false by a retake -- so **it links the artifact**, and a reader who follows the link + can settle it. Omitting digits removes the transcription failure, not the obligation to cite. - **When a figure must appear, it has exactly one home.** Prefer a committed capture the prose links to ([mutation-sweeps/2026-09-02/](../mutation-sweeps/2026-09-02) is this repository's existing example) over the same figure typed diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index ae2cf7c90..1dc377b9c 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -2000,8 +2000,10 @@ So the principle, which holds regardless of which mechanism is eventually chosen - **A claim belongs in prose.** "`reserving_mpsc` measured faster than `slotwise_mpsc` under contention, over a spread that overlaps the same-code control at every producer count" is a - claim. It contains no digits, so it cannot drift from the data -- it can only be wrong about it, - which a reader can see. + claim. It transcribes no figure, so it cannot drift from the artifact the way a pasted number + does -- but it is not thereby permanent: a retake can make it false, and a reader cannot tell from + the sentence alone. That is why the claim cites the artifact. Dropping the digits removes the + transcription failure and leaves the citation obligation exactly where it was. - **A number belongs in an artifact.** A measured cost, a capture's commit, a count of occurrences: one copy, with its provenance travelling *with* it rather than in a hand-maintained attribution table beside it. diff --git a/DESIGN-RATIONALE.md b/DESIGN-RATIONALE.md index 4b6d3b18d..9b7d24c27 100644 --- a/DESIGN-RATIONALE.md +++ b/DESIGN-RATIONALE.md @@ -205,6 +205,34 @@ following the rule that a binding which cannot be shown to fail is cosmetic. Fiv mutations -- three manifest values, a deleted claim, and a stale version planted in prose -- each produce a distinct, located failure. +## Why a measured figure is asked to have one home + +[DESIGN-NOTES.md](DESIGN-NOTES.md)'s restatement-drift section records the rule; this is how it was +reached, and what was rejected on the way. + +The evidence was a review history, not an argument. Across the rounds on PR #90, most findings were +not wrong measurements -- they were transcriptions that had drifted from the thing they restated: a +table disagreeing with its own copy, a control quoted for the wrong regime, a horizon stated in +minutes that the crate's own rate put at thirty-seven seconds. The measurements were fine. The +copies were not. + +Two weaker rules were considered and rejected. **"Keep the copies in sync"** is what had already +been happening, and the failure mode is that nothing enforces it; every drifted figure on that +branch was written by someone intending to keep it in sync. **"Never publish a figure"** fails the +other way: a caller choosing a layout needs a number, and hiding it behind a link that may not be +followed trades one failure for another. What survived is narrower -- the *figure* lives in a +committed artifact, and prose carries the *claim* plus a link to it. + +A correction from review is recorded with the rule itself: dropping the digits does **not** make a +claim permanent. A qualitative sentence cannot suffer transcription drift, because it transcribes +nothing, but a retake can still falsify it and a reader cannot see that from the sentence. So the +citation obligation is unchanged by the wording; only the transcription failure is removed. An +earlier draft of the rule said the digit-free form "cannot drift", which overstated it. + +The mechanism -- how a figure gets from an artifact into rendered prose -- is deliberately left +open; markdown has no include, and rustdoc's is whole-file. That is stated in the decision as an +unsettled trade rather than resolved here, and no work is scheduled against it. + ## References - [`QueueUserWorkItem` and `WT_TRANSFER_IMPERSONATION`](https://learn.microsoft.com/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-queueuserworkitem) From 6e8962ad073de2051744cd831b939bb1dba4a5a3 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:41:50 -0400 Subject: [PATCH 115/139] docs(queue): say where the Wide layout evidence actually lives The `Wide` throughput claim pointed a reader at `[readme-measurements]`, which is the README's `What was measured` table -- and that table carries the queue *shapes*, not the claim-word layouts. So the sentence cited evidence that does not contain the figures it rests on, and a caller deciding whether to enable `dwcas` had nothing to inspect. The claim now says where its evidence is and what state it is in: a seven-run sweep whose raw runs were never committed, and a three-run capture that was, at `windows-platform-probes`' `captures/2026-09-16-drained-handshake/`, whose `isolated.js` derives the per-count layout ratios against a same-code control. The two agree on the direction, and until the sweep is re-run with its data kept, the capture is the part of the claim a reader can check. Naming the gap is not closing it. It is better than a citation that silently misses. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 9967b79de..546ae153f 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -147,6 +147,16 @@ //! A longer horizon rather than the absence of one, and it moves with the //! caller's rate like every other figure in that column. //! +//! **Where the layout evidence is.** The `What was measured` table this section +//! links carries the queue *shapes*, not the claim-word layouts, so it does not +//! contain the `Wide` figures. Those come from two other places: a seven-run +//! sweep whose raw runs were never committed, and a three-run capture that was, +//! at `windows-platform-probes`' +//! `captures/2026-09-16-drained-handshake/`, whose `isolated.js` derives the +//! per-count layout ratios against a same-code control. The two agree on the +//! direction. Until the sweep is re-run with its data kept, the capture is the +//! part of this claim a reader can check. +//! //! The default remains `Balanced` so that no existing caller's behaviour //! changed when the choice was introduced. Under it, a queue driven past 2^32 //! pushes by two or more producers can **silently lose an item** -- the defect From 38196cb0423d13079ebd9e982fadbcb3e08f68be Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:46:15 -0400 Subject: [PATCH 116/139] test(probes): stop a malformed corpus from passing as the unknown-parallelism case `available_parallelism` was read with `as_u64().map(...)`, which returns `None` for a missing field, a string, a negative, or a fraction. `None` is also a real observation -- the processor-count query failed -- so a typo in the corpus rendered that case and passed, while the fixture said something else entirely. This file's contract is that malformed corpus data fails rather than testing a different case than it names; every other field already honours it, and the run rows are read through an exhaustive struct literal so a new field stops the build. This one field had a silent fallback. `null` is now matched explicitly and every other shape is rejected by name. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention/tests.rs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs index 2d3c23fb6..439736904 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -91,9 +91,28 @@ fn observation_from(value: &Value) -> Observation { Observation { isolated: rows("isolated"), drained: rows("drained"), - available_parallelism: value["available_parallelism"] - .as_u64() - .map(|count| count as usize), + // `null` is a real observation -- the processor-count query failed -- so + // it cannot also be what a typo produces. `as_u64().map(...)` would + // return `None` for a missing field, a string, a negative, or a + // fraction, rendering the unknown-parallelism case and passing, while + // the corpus said something else entirely. Every other malformed shape + // is rejected here so a broken fixture fails instead of testing a + // different case than it names. + available_parallelism: match &value["available_parallelism"] { + Value::Null => None, + other => Some( + other + .as_u64() + .unwrap_or_else(|| { + panic!( + "`available_parallelism` is a non-negative whole number or null, \ + not {other}" + ) + }) + .try_into() + .expect("a processor count fits a usize"), + ), + }, } } From bdaca658865a5bcdba8ff718b46f651ce257e56a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 22:46:15 -0400 Subject: [PATCH 117/139] docs(queue): qualify the shape comparison to the regime it was measured in `slotwise_mpsc`'s rustdoc said measurement "found this shape the slower of the two under contention" with no regime named. The crate's attributed table is the *isolated* regime, and the drained captures committed on this branch have rows going both ways on the same host -- so read without the qualifier the sentence is contradicted by the data in the same repository. This is the same defect corrected for the same-code control figures two commits ago: a regime-specific result stated as though it were regime-independent. The qualifier is now present and the note says why it is load-bearing. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/src/slotwise_mpsc.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/windows-waitable-queues/src/slotwise_mpsc.rs b/crates/windows-waitable-queues/src/slotwise_mpsc.rs index 65e0eea0e..c597a6773 100644 --- a/crates/windows-waitable-queues/src/slotwise_mpsc.rs +++ b/crates/windows-waitable-queues/src/slotwise_mpsc.rs @@ -197,9 +197,12 @@ pub fn bounded(capacity: usize) -> Result<(Producer, Consumer), Capacit /// branch on a field that is written once at construction. /// /// That avoidance is what distinguishes the two multi-producer shapes, but -/// **it is not what makes either one faster**: measurement found this shape the -/// slower of the two under contention on the host the crate's table was taken -/// on. See the crate +/// **it is not what makes either one faster**: in the **isolated** regime, +/// measurement found this shape the slower of the two under contention on the +/// host the crate's table was taken on. The qualifier is load-bearing -- the +/// crate's table is isolated, and the drained captures on the same host have +/// rows going both ways, so an unqualified reading of this sentence is +/// contradicted by the committed data. See the crate /// documentation's attributed table for the figures and the conditions they were /// taken under. (An earlier version of this sentence gave "by up to 6.4x", a /// figure from a two-host capture withdrawn for predating a correction to the From 24b2af831cc871241eea596b1b223693eaa86a37 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:02:11 -0400 Subject: [PATCH 118/139] fix(probes): repair form-feed corruption, and widen the did-not-run sentinel **Two source files carried an invisible control character, and I put it there.** The sink refactor two commits ago was applied with a PowerShell string in which `` `f `` is the *form feed* escape, so the literal "`fail`" was written to both capture scripts as U+000C followed by `ail`. The encoding check did not see it: a form feed is 7-bit ASCII, so the file is clean by that test. Repaired, and a scan of every `.rs`, `.js`, `.md`, `.json` and `.yml` in the tree confirms those two files were the only ones affected -- no other stray control byte exists. **`is_measured` guarded one of the four fields a renderer prints.** It tested `nanos_per_op` alone, so a row with a plausible median and a poisoned `ops_per_second` or range endpoint answered `true`, and `render_table` then formatted those fields directly. The sentinel exists precisely to stop a row that measured nothing from publishing a flattering cell, and it was letting three quarters of the row through. Now every field it prints must be finite and positive; `refusals` is an integer and carries no such value. **The corpus case for it was vacuous on the first attempt, which is worth recording.** Cloning the did-not-run case brought its generic `absent` needles along -- `0.00x`, `infx`, `NaN` -- and none of them match what a poisoned endpoint actually renders. The sabotage passed. The case now names the exact cells the old predicate produced (`6.0-0.0`, `-1.0-5.9`), and reverting `is_measured` turns it red. Verified in both directions rather than assumed, which is the only reason the first version's emptiness was caught at all. JSON cannot express infinity, so the non-finite half of the guard is not reachable from a corpus case; the same predicate covers it. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/isolated.js | 2 +- .../2026-09-16-drained-handshake/summarise.js | 2 +- .../src/bin/queue_contention/corpus.json | 848 ++++++++++++++++-- .../src/queue_contention.rs | 16 +- 4 files changed, 791 insertions(+), 77 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 1c4425ff0..902fe4e67 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -24,7 +24,7 @@ const fs = require("fs"); // One writer for the generated artifact, per this repository's output rule: no // formatting site picks a destination, so retargeting the report to a file is a -// change here and nowhere else. ail is the diagnostic path and stays separate +// change here and nowhere else. `fail` is the diagnostic path and stays separate // from the artifact, which is why they are two sinks rather than one with a flag. let sink = (text) => process.stdout.write(text + "\n"); const out = (text = "") => sink(text); diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 257c06569..ec025f1fe 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -10,7 +10,7 @@ const fs = require("fs"); // One writer for the generated artifact, per this repository's output rule: no // formatting site picks a destination, so retargeting the report to a file is a -// change here and nowhere else. ail is the diagnostic path and stays separate +// change here and nowhere else. `fail` is the diagnostic path and stays separate // from the artifact, which is why they are two sinks rather than one with a flag. let sink = (text) => process.stdout.write(text + "\n"); const out = (text = "") => sink(text); diff --git a/crates/windows-platform-probes/src/bin/queue_contention/corpus.json b/crates/windows-platform-probes/src/bin/queue_contention/corpus.json index e45442f26..6239776e4 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/corpus.json +++ b/crates/windows-platform-probes/src/bin/queue_contention/corpus.json @@ -29,39 +29,254 @@ "observation": { "available_parallelism": 8, "isolated": [ - ["baseline_fetch_add", 1, 2.3, 434782608.0, 0, 2.1, 2.5], - ["baseline_fetch_add", 2, 12.1, 165289256.0, 0, 11.0, 13.0], - ["slotwise_mpsc", 1, 6.3, 158730158.0, 0, 6.0, 6.8], - ["slotwise_mpsc", 2, 50.6, 39525691.0, 0, 19.3, 59.5], - ["reserving_mpsc", 1, 5.4, 185185185.0, 0, 5.2, 5.9], - ["reserving_mpsc", 2, 31.9, 31347962.0, 0, 22.5, 35.2], - ["permit_mpsc", 1, 7.9, 126582278.0, 0, 7.5, 8.3], - ["permit_mpsc", 2, 44.2, 22624434.0, 0, 37.4, 45.9], - ["reserving(32/32)", 1, 5.5, 181818181.0, 0, 5.3, 6.0], - ["reserving(32/32)", 2, 32.4, 30864197.0, 0, 23.0, 35.9], - ["reserving(16/48)", 1, 5.6, 178571428.0, 0, 5.4, 6.1], - ["reserving(16/48)", 2, 33.1, 30211480.0, 0, 23.4, 36.2], - ["reserving(8/56)", 1, 5.7, 175438596.0, 0, 5.5, 6.2], - ["reserving(8/56)", 2, 33.4, 29940119.0, 0, 23.8, 36.5] + [ + "baseline_fetch_add", + 1, + 2.3, + 434782608, + 0, + 2.1, + 2.5 + ], + [ + "baseline_fetch_add", + 2, + 12.1, + 165289256, + 0, + 11, + 13 + ], + [ + "slotwise_mpsc", + 1, + 6.3, + 158730158, + 0, + 6, + 6.8 + ], + [ + "slotwise_mpsc", + 2, + 50.6, + 39525691, + 0, + 19.3, + 59.5 + ], + [ + "reserving_mpsc", + 1, + 5.4, + 185185185, + 0, + 5.2, + 5.9 + ], + [ + "reserving_mpsc", + 2, + 31.9, + 31347962, + 0, + 22.5, + 35.2 + ], + [ + "permit_mpsc", + 1, + 7.9, + 126582278, + 0, + 7.5, + 8.3 + ], + [ + "permit_mpsc", + 2, + 44.2, + 22624434, + 0, + 37.4, + 45.9 + ], + [ + "reserving(32/32)", + 1, + 5.5, + 181818181, + 0, + 5.3, + 6 + ], + [ + "reserving(32/32)", + 2, + 32.4, + 30864197, + 0, + 23, + 35.9 + ], + [ + "reserving(16/48)", + 1, + 5.6, + 178571428, + 0, + 5.4, + 6.1 + ], + [ + "reserving(16/48)", + 2, + 33.1, + 30211480, + 0, + 23.4, + 36.2 + ], + [ + "reserving(8/56)", + 1, + 5.7, + 175438596, + 0, + 5.5, + 6.2 + ], + [ + "reserving(8/56)", + 2, + 33.4, + 29940119, + 0, + 23.8, + 36.5 + ] ], "drained": [ - ["slotwise_mpsc", 1, 11.4, 87719298.0, 0, 10.9, 12.1], - ["slotwise_mpsc", 2, 66.8, 29940119.0, 1200, 60.1, 70.2], - ["reserving_mpsc", 1, 26.2, 38167938.0, 0, 24.9, 27.8], - ["reserving_mpsc", 2, 64.2, 31152647.0, 1100, 58.3, 68.0], - ["permit_mpsc", 1, 64.3, 15552099.0, 0, 61.0, 67.1], - ["permit_mpsc", 2, 56.2, 35587188.0, 900, 51.4, 59.9], - ["reserving(32/32)", 1, 24.8, 40322580.0, 0, 23.6, 26.3], - ["reserving(32/32)", 2, 65.1, 30721966.0, 1150, 59.0, 69.1], - ["reserving(16/48)", 1, 26.8, 37313432.0, 0, 25.4, 28.2], - ["reserving(16/48)", 2, 67.9, 29455081.0, 1180, 61.2, 71.4], - ["reserving(8/56)", 1, 27.6, 36231884.0, 0, 26.1, 29.0], - ["reserving(8/56)", 2, 67.0, 29850746.0, 1160, 60.5, 70.8] + [ + "slotwise_mpsc", + 1, + 11.4, + 87719298, + 0, + 10.9, + 12.1 + ], + [ + "slotwise_mpsc", + 2, + 66.8, + 29940119, + 1200, + 60.1, + 70.2 + ], + [ + "reserving_mpsc", + 1, + 26.2, + 38167938, + 0, + 24.9, + 27.8 + ], + [ + "reserving_mpsc", + 2, + 64.2, + 31152647, + 1100, + 58.3, + 68 + ], + [ + "permit_mpsc", + 1, + 64.3, + 15552099, + 0, + 61, + 67.1 + ], + [ + "permit_mpsc", + 2, + 56.2, + 35587188, + 900, + 51.4, + 59.9 + ], + [ + "reserving(32/32)", + 1, + 24.8, + 40322580, + 0, + 23.6, + 26.3 + ], + [ + "reserving(32/32)", + 2, + 65.1, + 30721966, + 1150, + 59, + 69.1 + ], + [ + "reserving(16/48)", + 1, + 26.8, + 37313432, + 0, + 25.4, + 28.2 + ], + [ + "reserving(16/48)", + 2, + 67.9, + 29455081, + 1180, + 61.2, + 71.4 + ], + [ + "reserving(8/56)", + 1, + 27.6, + 36231884, + 0, + 26.1, + 29 + ], + [ + "reserving(8/56)", + 2, + 67, + 29850746, + 1160, + 60.5, + 70.8 + ] ] }, "expect": { - "aligned_tables": { "16/48 vs": 2, "reserving/slotwise": 1, "atomic floor": 1, "ns/op range": 2 }, - "contains": ["processors available to this process: 8"], + "aligned_tables": { + "16/48 vs": 2, + "reserving/slotwise": 1, + "atomic floor": 1, + "ns/op range": 2 + }, + "contains": [ + "processors available to this process: 8" + ], "absent": [] } }, @@ -71,39 +286,254 @@ "observation": { "available_parallelism": 8, "isolated": [ - ["baseline_fetch_add", 1, 2.3, 434782608.0, 0, 2.1, 2.5], - ["baseline_fetch_add", 2, 12.1, 165289256.0, 0, 11.0, 13.0], - ["slotwise_mpsc", 1, 6.0, 166666666.0, 0, 4.0, 6.0], - ["slotwise_mpsc", 2, 30.0, 66666666.0, 0, 4.0, 60.0], - ["reserving_mpsc", 1, 5.0, 200000000.0, 0, 4.0, 6.0], - ["reserving_mpsc", 2, 3000.0, 666666.0, 0, 40.0, 90000.0], - ["permit_mpsc", 1, 8.0, 125000000.0, 0, 4.0, 6.0], - ["permit_mpsc", 2, 3000.0, 666666.0, 0, 40.0, 90000.0], - ["reserving(32/32)", 1, 5.0, 200000000.0, 0, 4.0, 6.0], - ["reserving(32/32)", 2, 30.0, 66666666.0, 0, 4.0, 6.0], - ["reserving(16/48)", 1, 50.0, 20000000.0, 0, 40.0, 6000.0], - ["reserving(16/48)", 2, 3000.0, 666666.0, 0, 40.0, 90000.0], - ["reserving(8/56)", 1, 60.0, 16666666.0, 0, 40.0, 7000.0], - ["reserving(8/56)", 2, 3200.0, 625000.0, 0, 40.0, 95000.0] + [ + "baseline_fetch_add", + 1, + 2.3, + 434782608, + 0, + 2.1, + 2.5 + ], + [ + "baseline_fetch_add", + 2, + 12.1, + 165289256, + 0, + 11, + 13 + ], + [ + "slotwise_mpsc", + 1, + 6, + 166666666, + 0, + 4, + 6 + ], + [ + "slotwise_mpsc", + 2, + 30, + 66666666, + 0, + 4, + 60 + ], + [ + "reserving_mpsc", + 1, + 5, + 200000000, + 0, + 4, + 6 + ], + [ + "reserving_mpsc", + 2, + 3000, + 666666, + 0, + 40, + 90000 + ], + [ + "permit_mpsc", + 1, + 8, + 125000000, + 0, + 4, + 6 + ], + [ + "permit_mpsc", + 2, + 3000, + 666666, + 0, + 40, + 90000 + ], + [ + "reserving(32/32)", + 1, + 5, + 200000000, + 0, + 4, + 6 + ], + [ + "reserving(32/32)", + 2, + 30, + 66666666, + 0, + 4, + 6 + ], + [ + "reserving(16/48)", + 1, + 50, + 20000000, + 0, + 40, + 6000 + ], + [ + "reserving(16/48)", + 2, + 3000, + 666666, + 0, + 40, + 90000 + ], + [ + "reserving(8/56)", + 1, + 60, + 16666666, + 0, + 40, + 7000 + ], + [ + "reserving(8/56)", + 2, + 3200, + 625000, + 0, + 40, + 95000 + ] ], "drained": [ - ["slotwise_mpsc", 1, 11.0, 90909090.0, 0, 4.0, 6.0], - ["slotwise_mpsc", 2, 30.0, 66666666.0, 1200, 4.0, 60.0], - ["reserving_mpsc", 1, 26.0, 38461538.0, 0, 4.0, 60.0], - ["reserving_mpsc", 2, 3000.0, 666666.0, 1100, 40.0, 90000.0], - ["permit_mpsc", 1, 64.0, 15625000.0, 0, 4.0, 60.0], - ["permit_mpsc", 2, 3000.0, 666666.0, 900, 40.0, 90000.0], - ["reserving(32/32)", 1, 25.0, 40000000.0, 0, 4.0, 6.0], - ["reserving(32/32)", 2, 30.0, 66666666.0, 1150, 4.0, 6.0], - ["reserving(16/48)", 1, 250.0, 4000000.0, 0, 40.0, 9000.0], - ["reserving(16/48)", 2, 3000.0, 666666.0, 1180, 40.0, 90000.0], - ["reserving(8/56)", 1, 270.0, 3703703.0, 0, 40.0, 9000.0], - ["reserving(8/56)", 2, 3200.0, 625000.0, 1160, 40.0, 95000.0] + [ + "slotwise_mpsc", + 1, + 11, + 90909090, + 0, + 4, + 6 + ], + [ + "slotwise_mpsc", + 2, + 30, + 66666666, + 1200, + 4, + 60 + ], + [ + "reserving_mpsc", + 1, + 26, + 38461538, + 0, + 4, + 60 + ], + [ + "reserving_mpsc", + 2, + 3000, + 666666, + 1100, + 40, + 90000 + ], + [ + "permit_mpsc", + 1, + 64, + 15625000, + 0, + 4, + 60 + ], + [ + "permit_mpsc", + 2, + 3000, + 666666, + 900, + 40, + 90000 + ], + [ + "reserving(32/32)", + 1, + 25, + 40000000, + 0, + 4, + 6 + ], + [ + "reserving(32/32)", + 2, + 30, + 66666666, + 1150, + 4, + 6 + ], + [ + "reserving(16/48)", + 1, + 250, + 4000000, + 0, + 40, + 9000 + ], + [ + "reserving(16/48)", + 2, + 3000, + 666666, + 1180, + 40, + 90000 + ], + [ + "reserving(8/56)", + 1, + 270, + 3703703, + 0, + 40, + 9000 + ], + [ + "reserving(8/56)", + 2, + 3200, + 625000, + 1160, + 40, + 95000 + ] ] }, "expect": { - "aligned_tables": { "16/48 vs": 2, "reserving/slotwise": 1, "atomic floor": 1, "ns/op range": 2 }, - "contains": ["100.00x [6.67-22500.00]"], + "aligned_tables": { + "16/48 vs": 2, + "reserving/slotwise": 1, + "atomic floor": 1, + "ns/op range": 2 + }, + "contains": [ + "100.00x [6.67-22500.00]" + ], "absent": [] } }, @@ -113,27 +543,143 @@ "observation": { "available_parallelism": 4, "isolated": [ - ["baseline_fetch_add", 1, 2.3, 434782608.0, 0, 2.1, 2.5], - ["slotwise_mpsc", 1, 6.3, 158730158.0, 0, 6.0, 6.8], - ["reserving_mpsc", 1, 5.4, 185185185.0, 0, 5.2, 5.9], - ["permit_mpsc", 1, 0.0, 0.0, 0, 0.0, 0.0], - ["reserving(32/32)", 1, 5.5, 181818181.0, 0, 5.3, 6.0], - ["reserving(16/48)", 1, 0.0, 0.0, 0, 0.0, 0.0], - ["reserving(8/56)", 1, 5.7, 175438596.0, 0, 5.5, 6.2] + [ + "baseline_fetch_add", + 1, + 2.3, + 434782608, + 0, + 2.1, + 2.5 + ], + [ + "slotwise_mpsc", + 1, + 6.3, + 158730158, + 0, + 6, + 6.8 + ], + [ + "reserving_mpsc", + 1, + 5.4, + 185185185, + 0, + 5.2, + 5.9 + ], + [ + "permit_mpsc", + 1, + 0, + 0, + 0, + 0, + 0 + ], + [ + "reserving(32/32)", + 1, + 5.5, + 181818181, + 0, + 5.3, + 6 + ], + [ + "reserving(16/48)", + 1, + 0, + 0, + 0, + 0, + 0 + ], + [ + "reserving(8/56)", + 1, + 5.7, + 175438596, + 0, + 5.5, + 6.2 + ] ], "drained": [ - ["slotwise_mpsc", 1, 11.4, 87719298.0, 0, 10.9, 12.1], - ["reserving_mpsc", 1, 26.2, 38167938.0, 0, 24.9, 27.8], - ["permit_mpsc", 1, 0.0, 0.0, 0, 0.0, 0.0], - ["reserving(32/32)", 1, 24.8, 40322580.0, 0, 23.6, 26.3], - ["reserving(16/48)", 1, 0.0, 0.0, 0, 0.0, 0.0], - ["reserving(8/56)", 1, 27.6, 36231884.0, 0, 26.1, 29.0] + [ + "slotwise_mpsc", + 1, + 11.4, + 87719298, + 0, + 10.9, + 12.1 + ], + [ + "reserving_mpsc", + 1, + 26.2, + 38167938, + 0, + 24.9, + 27.8 + ], + [ + "permit_mpsc", + 1, + 0, + 0, + 0, + 0, + 0 + ], + [ + "reserving(32/32)", + 1, + 24.8, + 40322580, + 0, + 23.6, + 26.3 + ], + [ + "reserving(16/48)", + 1, + 0, + 0, + 0, + 0, + 0 + ], + [ + "reserving(8/56)", + 1, + 27.6, + 36231884, + 0, + 26.1, + 29 + ] ] }, "expect": { - "aligned_tables": { "16/48 vs": 2, "reserving/slotwise": 1, "atomic floor": 1, "ns/op range": 2 }, - "contains": ["--"], - "absent": ["0.00x", " 0.0 ", "infx", "NaN"] + "aligned_tables": { + "16/48 vs": 2, + "reserving/slotwise": 1, + "atomic floor": 1, + "ns/op range": 2 + }, + "contains": [ + "--" + ], + "absent": [ + "0.00x", + " 0.0 ", + "infx", + "NaN" + ] } }, { @@ -145,12 +691,166 @@ "drained": [] }, "expect": { - "aligned_tables": { "16/48 vs": 2, "reserving/slotwise": 1, "atomic floor": 1 }, + "aligned_tables": { + "16/48 vs": 2, + "reserving/slotwise": 1, + "atomic floor": 1 + }, "contains": [ "processors available to this process: unknown (the query failed)", "ns/op range" ], - "absent": ["0.00x", "infx", "NaN"] + "absent": [ + "0.00x", + "infx", + "NaN" + ] + } + }, + { + "name": "a_row_whose_median_looks_fine_but_whose_endpoints_do_not", + "why": "A median is not the only field a row publishes. `render_table` prints ops/sec and both range endpoints directly once `is_measured` says the row counts, so a plausible median beside a poisoned endpoint reached the report as an ordinary-looking cell. These rows keep an ordinary median and spoil one other field each: a zero slowest endpoint, a negative fastest one, a zero ops/sec. JSON cannot express infinity, so the non-finite half of the guard is not reachable from a corpus case and is covered by the same predicate.", + "observation": { + "available_parallelism": 4, + "isolated": [ + [ + "baseline_fetch_add", + 1, + 2.3, + 0, + 0, + 2.1, + 2.5 + ], + [ + "slotwise_mpsc", + 1, + 6.3, + 158730158, + 0, + 6, + 0 + ], + [ + "reserving_mpsc", + 1, + 5.4, + 185185185, + 0, + -1, + 5.9 + ], + [ + "permit_mpsc", + 1, + 0, + 0, + 0, + 0, + 0 + ], + [ + "reserving(32/32)", + 1, + 5.5, + 181818181, + 0, + 5.3, + 6 + ], + [ + "reserving(16/48)", + 1, + 0, + 0, + 0, + 0, + 0 + ], + [ + "reserving(8/56)", + 1, + 5.7, + 175438596, + 0, + 5.5, + 6.2 + ] + ], + "drained": [ + [ + "slotwise_mpsc", + 1, + 11.4, + 87719298, + 0, + 10.9, + 12.1 + ], + [ + "reserving_mpsc", + 1, + 26.2, + 38167938, + 0, + 24.9, + 27.8 + ], + [ + "permit_mpsc", + 1, + 0, + 0, + 0, + 0, + 0 + ], + [ + "reserving(32/32)", + 1, + 24.8, + 40322580, + 0, + 23.6, + 26.3 + ], + [ + "reserving(16/48)", + 1, + 0, + 0, + 0, + 0, + 0 + ], + [ + "reserving(8/56)", + 1, + 27.6, + 36231884, + 0, + 26.1, + 29 + ] + ] + }, + "expect": { + "aligned_tables": { + "16/48 vs": 2, + "reserving/slotwise": 1, + "atomic floor": 1, + "ns/op range": 2 + }, + "contains": [ + "--" + ], + "absent": [ + "6.0-0.0", + "-1.0-5.9", + "0.00x", + "infx", + "NaN" + ] } } ] diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 0d61f17b4..5ada9e0a7 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -217,9 +217,23 @@ impl Run { /// never ran. Fixing that one accessor left the same hole in four other /// paths, because the sentinel was a convention rather than a definition. /// A renderer can now only get this wrong by not asking. + /// + /// **Every field a renderer prints, not just the median.** An earlier + /// version tested `nanos_per_op` alone, so a row with a plausible median and + /// a poisoned `ops_per_second` or range endpoint answered `true` and + /// [`render_table`] then formatted those fields directly -- publishing `NaN` + /// or `inf` in a column of measurements, which is the failure the sentinel + /// exists to prevent. `refusals` is an integer and carries no such value. #[must_use] pub fn is_measured(&self) -> bool { - self.nanos_per_op.is_finite() && self.nanos_per_op > 0.0 + [ + self.nanos_per_op, + self.ops_per_second, + self.fastest_nanos_per_op, + self.slowest_nanos_per_op, + ] + .iter() + .all(|value| value.is_finite() && *value > 0.0) } /// The spread across this configuration's repetitions, as a multiple. From e9b69e659930e77a758963436c56f5deea59f3a0 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:02:12 -0400 Subject: [PATCH 119/139] docs(queue): give the reservation ceiling its capacity bound, and link the capture Two leftovers from corrections made one site short. The choice guide called 65,535 and 255 "a lower ceiling on outstanding reservations", which is the same unqualified form corrected in the capacity paragraph two commits ago: those are *field* ceilings, and what a given queue can hold is the lesser of that and its capacity. A capacity-64 queue reaches neither number under either layout, so the guidance contradicted the contract three screens above it. The `Wide` evidence pointer named the capture directory in backticks rather than linking it, so the one artifact a reader can actually run was not reachable from the documentation that cites it. Now a relative link, verified to resolve. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/src/lib.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 546ae153f..2ab5a4593 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -151,8 +151,9 @@ //! links carries the queue *shapes*, not the claim-word layouts, so it does not //! contain the `Wide` figures. Those come from two other places: a seven-run //! sweep whose raw runs were never committed, and a three-run capture that was, -//! at `windows-platform-probes`' -//! `captures/2026-09-16-drained-handshake/`, whose `isolated.js` derives the +//! at +//! [captures/2026-09-16-drained-handshake/](../../windows-platform-probes/captures/2026-09-16-drained-handshake/README.md), +//! whose `isolated.js` derives the //! per-count layout ratios against a same-code control. The two agree on the //! direction. Until the sweep is re-run with its data kept, the capture is the //! part of this claim a reader can check. @@ -330,8 +331,11 @@ //! implements it too, and the experimental `permit_mpsc` exposes its own //! `reserve`.) Wanting it no longer means accepting the default layout's //! recurrence, but the trade is not gone -- it changes axis: a deeper position -//! is paid for with a lower ceiling on outstanding reservations, 65,535 under -//! `Enduring` and 255 under `Perpetual` against `u32::MAX` under the default. +//! is paid for with a lower ceiling on outstanding reservations -- a *field* +//! ceiling of 65,535 under +//! `Enduring` and 255 under `Perpetual` against `u32::MAX` under the default, +//! with the count a given queue can actually hold being the lesser of that and +//! its capacity. //! - **[`spsc`] requires exactly one producer and one consumer**, and does less //! work than either MPSC shape because of it. //! From e441539a08f06b6f17739b2c90f17308992d678a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:02:12 -0400 Subject: [PATCH 120/139] docs: stop counting the documents that name M31.6 The convergence note said "five documents name `M31.6` as its owner". There are four, and the count was wrong the moment it was written -- in a section whose whole argument is that a figure transcribed into prose drifts from the thing it describes. It is now "several", which cannot rot. Also fixes a possessive with no noun after it: "rustdoc's is whole-file" -> "rustdoc's include is whole-file". The substantive half of that finding stands and is not addressed here: the loom verification remains unscheduled, `windows-waitable-queues` still has no CHECKLIST.md, and four documents still describe `M31.6` as though it were tracked. Creating that item decides the shape of another component's plan, which is the engineer's call; the note says the work is unscheduled so the gap is visible in the meantime. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 5 +++-- DESIGN-RATIONALE.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 1dc377b9c..94e81aaf8 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1944,8 +1944,9 @@ Three conclusions, of which the middle one is the one that changes practice. **Formal specification and prose reduction address different classes.** TLA+ and `loom` ([D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31)) target algorithm properties. Neither -has been run, and neither is scheduled: `D-31` records the `loom` verification as planned, and five -documents name `M31.6` as its owner, but no CHECKLIST contains that item -- `windows-waitable-queues` +has been run, and neither is scheduled: `D-31` records the `loom` verification as planned, and +several documents name `M31.6` as its owner, but no CHECKLIST contains that item -- +`windows-waitable-queues` has no CHECKLIST.md at all, only a COMPLETED-CHECKLIST.md. So what can be said about that class is that it produced no findings in any review round of PR #90 while carrying one known unfound defect, which is a statement about the reviews rather than a diff --git a/DESIGN-RATIONALE.md b/DESIGN-RATIONALE.md index 9b7d24c27..4983c5d11 100644 --- a/DESIGN-RATIONALE.md +++ b/DESIGN-RATIONALE.md @@ -230,7 +230,7 @@ citation obligation is unchanged by the wording; only the transcription failure earlier draft of the rule said the digit-free form "cannot drift", which overstated it. The mechanism -- how a figure gets from an artifact into rendered prose -- is deliberately left -open; markdown has no include, and rustdoc's is whole-file. That is stated in the decision as an +open; markdown has no include, and rustdoc's include is whole-file. That is stated in the decision as an unsettled trade rather than resolved here, and no work is scheduled against it. ## References From e7d0a2854c90e178b23c99c0f27a81e9bf541c68 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:08:31 -0400 Subject: [PATCH 121/139] fix(probes): reject fixture and capture values that are not what they claim Three ways malformed input was normalised into well-formed input rather than rejected. All three share a shape: a conversion that *succeeds* on bad data. **`as` on a `f64` count.** The corpus read producer and refusal counts with `number(i) as usize` / `as u64`, which truncates a fraction and saturates a negative -- so a fixture row saying `1.5` exercised producer count 1, and `-3` exercised 0, while the corpus claimed something else. Both now assert the value is finite, non-negative and integral before converting. Sabotage: `1.5` reports `field 1 of a run is a non-negative whole number, not 1.5`. **Indexing a missing key yields `Value::Null`.** Last round's fix separated an explicit `null` -- the real "processor-count query failed" observation -- from a typo, and the comment I wrote said exactly that. It was still wrong: `value[...]` returns `Null` for an *absent* key too, so an omitted field remained indistinguishable from an explicit one, which is the same conflation one layer up. `get` separates them. Sabotage: deleting the key reports `a case states available_parallelism, using null where the query failed`. **A value-level integer test normalises the label it rejects.** `summarise.js` read producer labels through `finite`, so a capture written `2.0` parsed to 2 and matched the expected count. The first fix used `Number.isInteger`, which is no better: `Number("2.0")` *is* 2, and `isInteger(2)` is true. Caught by running the sabotage, which passed. The check is now on the text -- a producer count is written as digits -- and `2.0` reports `is not a whole number`. `summary.txt` still reproduces byte-identically. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/summarise.js | 19 +++++++++++-- .../src/bin/queue_contention/tests.rs | 28 +++++++++++++++---- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index ec025f1fe..46a4e9cf7 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -64,6 +64,21 @@ function positive(text, what) { return value; } +// A producer count is a positive whole number, and is used as a `Map` key. Left +// to `finite`, a label of `1.0` becomes the number 1 and matches the expected +// count 1, so a malformed capture would be normalised into a well-formed one on +// the way past the completeness check. +function wholeCount(text, what) { + // The check is on the TEXT, not the parsed value: `Number("2.0")` is 2 and + // `Number.isInteger(2)` is true, so a value-level test normalises the very + // label it is meant to reject. A producer count is written as digits. + if (!/^\d+$/.test(text)) { + problems.push(`${what}: ${JSON.stringify(text)} is not a whole number`); + return null; + } + return positive(text, what); +} + // A ratio triple must be ordered and must contain its own point estimate. // `positive` accepts each number on its own, so `2.00x [3.00-1.00]` passes // three separate checks and is still not a interval any instrument produced. @@ -97,7 +112,7 @@ function drainedLayout(lines, path) { const rows = new Map(); for (const line of lines.slice(start + 2, start + 8)) { const fields = line.trim().split(/\s+/); - const producers = finite(fields[0], `${path}: a drained layout producer count`); + const producers = wholeCount(fields[0], `${path}: a drained layout producer count`); if (producers === null) continue; const where = `${path}, drained layout, ${producers} producers`; const narrowNanos = positive(fields[1], `${where}: the 32/32 cost`); @@ -143,7 +158,7 @@ function drainedComparison(lines, path) { const rows = new Map(); for (const line of lines.slice(start + 2, start + 8)) { const fields = line.trim().split(/\s+/); - const producers = finite(fields[0], `${path}: a comparison producer count`); + const producers = wholeCount(fields[0], `${path}: a comparison producer count`); if (producers === null) continue; const reserving = positive( fields[2], diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs index 439736904..5bd8c5d0c 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -59,6 +59,17 @@ fn run_from(value: &Value) -> Run { .as_f64() .unwrap_or_else(|| panic!("field {index} of a run is a number")) }; + // `as` truncates a fraction and saturates a negative, so `1.5` would become + // producer count 1 and `-3` would become 0 -- a fixture exercising a case it + // does not name, silently. Counts are checked for being counts first. + let whole = |index: usize| -> u64 { + let value = number(index); + assert!( + value.is_finite() && value >= 0.0 && value.fract() == 0.0, + "field {index} of a run is a non-negative whole number, not {value}" + ); + value as u64 + }; Run { // Leaked so the fixture can hand back the `&'static str` the field // wants. A test process is the one place that is the cheap answer, and @@ -70,10 +81,10 @@ fn run_from(value: &Value) -> Run { .to_owned() .into_boxed_str(), ), - producers: number(1) as usize, + producers: usize::try_from(whole(1)).expect("a producer count fits a usize"), nanos_per_op: number(2), ops_per_second: number(3), - refusals: number(4) as u64, + refusals: whole(4), fastest_nanos_per_op: number(5), slowest_nanos_per_op: number(6), } @@ -95,10 +106,15 @@ fn observation_from(value: &Value) -> Observation { // it cannot also be what a typo produces. `as_u64().map(...)` would // return `None` for a missing field, a string, a negative, or a // fraction, rendering the unknown-parallelism case and passing, while - // the corpus said something else entirely. Every other malformed shape - // is rejected here so a broken fixture fails instead of testing a - // different case than it names. - available_parallelism: match &value["available_parallelism"] { + // the corpus said something else entirely. + // + // Indexing with `[]` yields `Value::Null` for an absent key, which would + // have made an *omitted* field indistinguishable from an explicit one -- + // the same conflation one layer up, and exactly what the sentence above + // claims is impossible. `get` separates them. + available_parallelism: match value.get("available_parallelism").unwrap_or_else(|| { + panic!("a case states `available_parallelism`, using null where the query failed") + }) { Value::Null => None, other => Some( other From 891c226b6cbb41b6bfe9886790731b91208f527f Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:08:31 -0400 Subject: [PATCH 122/139] docs: stop quoting a ratio the same section declines to stand behind The prose-volume section opened by stating that prose here "runs at somewhat less than a line per line of code", then said that ratio is the wrong thing to watch. The figure has no artifact and no derivation, so it was an uncited measurement sitting inside the argument against uncited measurements -- and its own paragraph immediately disclaims any interest in it. It now says the ratio was looked at and set aside, and says why it is not quoted. The reasoning is unchanged; what is gone is a number nobody can check that the section never used. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 94e81aaf8..7ca6db327 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1873,8 +1873,10 @@ surface. ### The measurement, and why it is not written down here -Prose in this workspace runs at somewhat less than a line per line of code, counting Rust comment -lines and markdown together. That ratio turns out to be the wrong thing to watch. +Prose volume was looked at first and set aside: whatever the ratio of prose to code is here, it is +not what the findings track. That ratio is deliberately not quoted, because quoting a measurement +this section takes no position on would be an uncited figure inside the argument against uncited +figures. The thing to watch is that in `windows-waitable-queues`, a handful of single facts -- `Perpetual`'s reservation-count ceiling, `Balanced`'s recurrence horizon, `Perpetual`'s position span, `Balanced`'s From be45b426253d27a52409d76803d2786de904769a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:19:17 -0400 Subject: [PATCH 123/139] fix(probes): reject a duplicated capture, and a spread built from a bad endpoint **The same capture given twice was counted as two independent runs.** Both scripts report `paths.length` as the run count, and the control range prints its `(n)` from it, so `node summarise.js run1.txt run1.txt` certified repeated bytes as agreement -- with a zero-width control, which is the most reassuring thing that column can show. A typo could make the capture look more reproducible than it is, which is the one property these artifacts exist to establish. Both now reject a repeated path and exit 2. **`spread` divided by a non-positive slowest endpoint.** The guard required both endpoints finite and the *fastest* positive, so a zero slowest divided cleanly to a spread of zero, and a negative one to a negative spread. `is_measured` keeps those rows out of the report, but `spread` is public and answers callers directly, so the renderer's guard is not the accessor's. Both committed outputs still reproduce byte-identically; the duplicate-path guard was verified by running each script against the same file twice. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/isolated.js | 14 ++++++++++++++ .../2026-09-16-drained-handshake/summarise.js | 13 +++++++++++++ .../src/queue_contention.rs | 6 ++++++ 3 files changed, 33 insertions(+) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 902fe4e67..d2ea9a6f7 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -35,6 +35,20 @@ if (files.length === 0) { fail("usage: node isolated.js [run.txt ...]"); process.exit(2); } +// `files.length` is reported as the number of independent runs, so the same +// path given twice would publish a two-run comparison -- with a zero-width +// control range -- derived from one set of bytes. A typo must not make the +// capture look more reproducible than it is. +{ + const seen = new Set(); + for (const file of files) { + if (seen.has(file)) { + fail(`the same capture was given more than once: ${file}`); + process.exit(2); + } + seen.add(file); + } +} const COUNTS = [1, 2, 4, 8, 16, 32]; const DEFAULT_LAYOUT = "reserving(32/32)"; diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 46a4e9cf7..76068193d 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -188,6 +188,19 @@ if (paths.length === 0) { fail("usage: node summarise.js [run.txt ...]"); process.exit(2); } +// The run count and the control's `(n)` are both `paths.length`, so the same +// capture given twice would be certified as two independent runs -- repeated +// bytes reported as agreement. +{ + const seen = new Set(); + for (const path of paths) { + if (seen.has(path)) { + fail(`the same capture was given more than once: ${path}`); + process.exit(2); + } + seen.add(path); + } +} const layouts = []; const controls = []; for (const path of paths) { diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 5ada9e0a7..13e30d800 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -261,6 +261,12 @@ impl Run { if self.fastest_nanos_per_op.is_finite() && self.fastest_nanos_per_op > 0.0 && self.slowest_nanos_per_op.is_finite() + // Positive too, not merely finite: a zero slowest endpoint divides + // cleanly to a spread of zero -- the most reassuring value this + // column can hold -- and a negative one yields a negative spread, + // which is not a dispersion at all. `is_measured` keeps both out of + // the report, but `spread` is public and answers callers directly. + && self.slowest_nanos_per_op > 0.0 { Some(self.slowest_nanos_per_op / self.fastest_nanos_per_op) } else { From 5118d35df4f5fbdb980b2f0f02ffe02dcd8f90dd Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:19:18 -0400 Subject: [PATCH 124/139] ci: build the probes where a 128-bit exchange is unavailable `windows-platform-probes` takes the queue crate's `dwcas` feature from two `[target.'cfg(...)']` tables, so the manifest predicate and the source's `cfg(feature = "dwcas")` gating have to agree. Nothing in CI could tell if they stopped agreeing: every existing job is x86-64 or aarch64 with `cmpxchg16b` enabled, where the feature is always on and the ungated path is never compiled. The two configurations the gating exists for are now built -- i686, which has no `AtomicU128` at all, and x86-64 with `-C target-feature=-cmpxchg16b`. Both were verified by hand when the gating was written, which is exactly the kind of check that stops being run. **Confirmed non-vacuous before committing.** `cargo tree --invert portable-atomic` for this package prints the dependency on a plain x86-64 build and "nothing to print" under `-C target-feature=-cmpxchg16b`, so the second job really does compile the path with the feature off rather than repeating what the other jobs already cover. Both checks pass on the current tree. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a421ec082..fb18cf5dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -619,6 +619,37 @@ jobs: - name: cargo check (MSRV) run: cargo check --workspace --all-targets --all-features --locked + dwcas-gating: + name: probes build where a 128-bit exchange is unavailable + runs-on: windows-latest + # See build-test's comment: forces the floating stable toolchain over the + # repo's rust-toolchain.toml pin. + env: + RUSTUP_TOOLCHAIN: stable + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + targets: i686-pc-windows-msvc + - uses: Swatinem/rust-cache@v2 + # `windows-platform-probes` takes the queue crate's `dwcas` feature from + # two `[target.'cfg(...)']` tables, so the manifest predicate and the + # source's `cfg(feature = "dwcas")` gating have to agree. Nothing else in + # CI builds a configuration where they can disagree: every other job is + # x86-64 or aarch64 with `cmpxchg16b` on, where the feature is always + # enabled and the ungated path is never compiled. + # + # These two are the configurations the gating exists for. A 32-bit target + # has no `AtomicU128` at all, and `-C target-feature=-cmpxchg16b` removes + # it from x86-64 -- each was verified by hand when the gating was written, + # which is precisely the kind of check that stops being run. + - name: cargo check (i686, no 128-bit atomic) + run: cargo check -p windows-platform-probes --all-targets --target i686-pc-windows-msvc --locked + - name: cargo check (x86-64 with cmpxchg16b disabled) + env: + RUSTFLAGS: "-C target-feature=-cmpxchg16b" + run: cargo check -p windows-platform-probes --all-targets --target x86_64-pc-windows-msvc --locked + alloc-only: name: wtf-string (alloc-only, no_std) runs-on: windows-latest From e2a212e55772681d9a9a7729894847d01d7c64b7 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:19:18 -0400 Subject: [PATCH 125/139] docs: keep the pre-handshake qualifier, and stop deciding for the reader **Five sites said a figure was "the conservative direction for a hazard".** That tells the reader what the arithmetic means for them, which is what `D-no-client-prescriptions` and this PR's own presentation rule forbid in markdown and rustdoc. Review named two; the proposition was in five, across the README, the crate doc twice, and `reserving_mpsc` twice. All now state the mechanical fact -- the pre-correction rate names a shorter horizon than the corrected one gives -- and leave the reader to decide what that is worth. **The capture's closing paragraph dropped a qualifier it uses everywhere else.** It said the claim that nothing separates in the drained regime "rests on the seven-run sweep", without saying that sweep is the *pre-handshake* reading of a procedure `M4.3` has since changed. In a paragraph whose purpose is to stop a withdrawn result being carried forward, the missing word was the whole risk. **A design note pointed at a file that does not exist.** It named `CHECKLIST.md` while making the point that no such checklist exists, leaving an unopenable reference. It now says there is no open checklist and links the archive that does exist. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 7 ++++--- .../captures/2026-09-16-drained-handshake/README.md | 5 ++++- crates/windows-waitable-queues/README.md | 3 +-- crates/windows-waitable-queues/src/lib.rs | 4 ++-- crates/windows-waitable-queues/src/reserving_mpsc.rs | 4 ++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 7ca6db327..17646dae9 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1947,9 +1947,10 @@ Three conclusions, of which the middle one is the one that changes practice. **Formal specification and prose reduction address different classes.** TLA+ and `loom` ([D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31)) target algorithm properties. Neither has been run, and neither is scheduled: `D-31` records the `loom` verification as planned, and -several documents name `M31.6` as its owner, but no CHECKLIST contains that item -- -`windows-waitable-queues` -has no CHECKLIST.md at all, only a COMPLETED-CHECKLIST.md. So what can be said about that class is +several documents name `M31.6` as its owner, but no checklist contains that item -- +`windows-waitable-queues` has an archive, +[COMPLETED-CHECKLIST.md](crates/windows-waitable-queues/COMPLETED-CHECKLIST.md), and no open +checklist at all. So what can be said about that class is that it produced no findings in any review round of PR #90 while carrying one known unfound defect, which is a statement about the reviews rather than a result from either instrument. Restatement targets documented facts, diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md index 30744f4f3..d824500af 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md @@ -90,4 +90,7 @@ three samples is not a band to judge anything against. So three runs do not settle the drained comparison in either direction. This capture reports figures; the claim that nothing separates in the drained regime -rests on the seven-run sweep, which this does not replace. +is the **pre-handshake** reading, resting on the seven-run sweep, which this does +not replace and which measured a probe `M4.3` has since changed. Carrying that +reading forward as a current statement about the drained regime is the thing this +paragraph exists to prevent. diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index 5ed563de4..e6ce53aa5 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -193,8 +193,7 @@ above discloses. Two producers is the smallest count that can trigger the defect at all. **That rate predates a correction to the probe's timing window** and is kept as a floor for the reason the layout table above gives: the correction lowers the rate and lengthens the horizon, so -these figures say the wrap arrives sooner than it does, which is the -conservative direction for a hazard. That is sustained throughput, not a total +these figures name a shorter horizon than the corrected rate gives. That is sustained throughput, not a total accumulated over an uptime. Reaching the wrap is necessary but not sufficient: a producer must also be stalled inside a window a few instructions wide. Rare, but a preemption is enough, and "rare" over billions of pushes is not "never". diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 2ab5a4593..28d910474 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -106,7 +106,7 @@ //! rate predates a correction to the probe's timing window**, which had //! overstated throughput -- so the true sustained rate is lower and these //! horizons longer. They are kept as a floor, saying the wrap arrives sooner -//! than it does, which is the conservative direction for a hazard. The horizon +//! than it does -- a shorter horizon than the corrected rate gives. The horizon //! that matters is the one on your hardware at your rate. //! //! The reservation-count column is the field's ceiling rather than the count any @@ -183,7 +183,7 @@ //! correction to the probe's timing window** and is kept as a floor for the //! reason the layout table above gives: the correction lowers the rate and //! lengthens the horizon, so these figures say the wrap arrives sooner than it -//! does, which is the conservative direction for a hazard. That is sustained +//! does -- a shorter horizon than the corrected rate gives. That is sustained //! throughput, //! not a total accumulated over an uptime. Reaching the wrap is necessary but //! not sufficient: a producer must also be stalled inside a window a few diff --git a/crates/windows-waitable-queues/src/reserving_mpsc.rs b/crates/windows-waitable-queues/src/reserving_mpsc.rs index 0f5a15839..b9b1c8cd7 100644 --- a/crates/windows-waitable-queues/src/reserving_mpsc.rs +++ b/crates/windows-waitable-queues/src/reserving_mpsc.rs @@ -24,7 +24,7 @@ //! *sustained* pushing at the rate [`ClaimLayout`] discloses; two producers is //! the smallest count that can trigger it. That rate predates a correction to the probe's timing window, //! so it is a floor rather than a forecast -- the correction lowers the rate -//! and lengthens the horizon, which is the conservative direction for a hazard; +//! and lengthens the horizon, so the figures below name a shorter one than it gives; //! see [`ClaimLayout`]. The wrap alone is not enough -- a producer must also stall //! inside a window a few instructions wide -- but a preemption suffices. //! @@ -255,7 +255,7 @@ use crate::options::Options; /// which had overstated throughput. The correction therefore moves the true /// sustained rate *down* and these horizons *up*, so the figures above remain a /// floor -- they say the wrap arrives sooner than it does, which is the -/// conservative direction for a hazard. They have not been recomputed, because +/// corrected rate gives. They have not been recomputed, because /// the horizon a caller needs is the one on their own hardware and at their own /// rate; the arithmetic is field width divided by rate. /// From 0fe4230d484193129311870e7bc0add9df54d034 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:27:07 -0400 Subject: [PATCH 126/139] docs(queue): make the Wide claim reachable, and qualify the regime in a dev note **The rustdoc link I added last round is broken on docs.rs.** It pointed into `windows-platform-probes` with a relative path, and that crate is `publish = false` -- so the target is not part of the published documentation and a reader of the API docs cannot follow it. Navigable in the repository, dead where it is actually read. Now an absolute repository URL, matching the `readme-measurements` links beside it. **The README made the same `Wide` claim with no artifact at all**, which is the site-short pattern again: the pointer went into the crate doc last round and not into the README that carries the same sentence. It now names the same two sources and links the committed capture. **A developer note left the regime off.** `slotwise_mpsc` said measurement found the shape "slower than `reserving_mpsc` under contention" with no qualifier, while the committed drained capture has it *faster* at one producer (10.5 against 24.5 ns/op) and slower at others. The finding is the isolated one; the note says so now, and says the drained data goes both ways. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-waitable-queues/README.md | 8 +++++++- crates/windows-waitable-queues/src/lib.rs | 2 +- crates/windows-waitable-queues/src/slotwise_mpsc.rs | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/windows-waitable-queues/README.md b/crates/windows-waitable-queues/README.md index e6ce53aa5..805c2998f 100644 --- a/crates/windows-waitable-queues/README.md +++ b/crates/windows-waitable-queues/README.md @@ -240,7 +240,13 @@ stops at 64 bits -- so the double-width compare-and-swap comes from twenty years before its claim position recurs with no dependency, though what that costs in throughput is not established, while under `Wide` the whole push path was measured as slower at every producer count measured -- smallest at one -or two, several times by thirty-two, in the isolated regime. What `Wide` provides +or two, several times by thirty-two, in the isolated regime. That claim rests on +a seven-run sweep whose raw runs were never committed, and on a three-run capture +that was: +[captures/2026-09-16-drained-handshake/](../windows-platform-probes/captures/2026-09-16-drained-handshake/README.md), +whose `isolated.js` derives the per-count layout ratios against a same-code +control. The two agree on the direction; the capture is the part a reader can +run. What `Wide` provides that the `u64` layouts do not is a 64-bit position: the recurrence moves to 2^64 pushes -- about 5,000 years at the same rate the table above uses, rather than the twenty `Perpetual` buys. That is a longer horizon, not the absence of diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 28d910474..7a8ba80a5 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -152,7 +152,7 @@ //! contain the `Wide` figures. Those come from two other places: a seven-run //! sweep whose raw runs were never committed, and a three-run capture that was, //! at -//! [captures/2026-09-16-drained-handshake/](../../windows-platform-probes/captures/2026-09-16-drained-handshake/README.md), +//! [captures/2026-09-16-drained-handshake/](https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md), //! whose `isolated.js` derives the //! per-count layout ratios against a same-code control. The two agree on the //! direction. Until the sweep is re-run with its data kept, the capture is the diff --git a/crates/windows-waitable-queues/src/slotwise_mpsc.rs b/crates/windows-waitable-queues/src/slotwise_mpsc.rs index c597a6773..d1efe1b3a 100644 --- a/crates/windows-waitable-queues/src/slotwise_mpsc.rs +++ b/crates/windows-waitable-queues/src/slotwise_mpsc.rs @@ -560,7 +560,10 @@ impl Producer { // asked for the answer. // // Note what this property does *not* buy: measurement found this shape - // slower than `reserving_mpsc` under contention despite it. Why that is + // slower than `reserving_mpsc` under contention in the ISOLATED regime + // despite it -- the drained capture on the same host has rows going both + // ways, so the unqualified claim is contradicted by committed data. Why + // even the isolated result is // so is not established -- the probe times the complete push, so the // sequence read // is one term among several and is never isolated. An earlier version of From 7b159630b56e1db5d2bf3af8aac920a8003bd3b2 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:27:07 -0400 Subject: [PATCH 127/139] docs(probes): give the variance figures one home instead of two The rationale retyped the measurements it was explaining -- the noise floor, the seven-run spread, the layout medians, the control's excursions -- while the design note's tables already own them. Two hand-maintained homes for the same numbers, inside the section arguing that a figure with two homes drifts. Reported repeatedly, and declined repeatedly on the grounds that the seven-run sweep has no committed capture and its raw runs cannot be recovered. That was an answer to only half of it: the sweep's missing artifact is a real and separate gap, but nothing about it required the figures to appear *twice*. The rationale now states the shapes -- the ratios were several times the quoted floor, seven runs put the spread roughly an order of magnitude wider than two runs had, the inverted claim would have sat inside the control's own excursions -- and points at the tables for the numbers. The sweep's gap is unchanged and stays recorded where the figures live: its raw runs were never committed, so that table cannot be recomputed. Removing the duplicate does not close that; it stops the same uncheckable number being maintained in two places while it remains open. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-RATIONALE.md | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 386be7098..034557fd9 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -539,19 +539,26 @@ that 16/48 and 8/56 "track the default within noise" -- and therefore that buyin twenty years of counter headroom cost nothing. Two independent defects sat under that sentence. -The first was arithmetic-shaped: the table directly beneath it showed 1.21x and -1.13x, against a noise floor the same document put at 2-6%. The prose +The first was arithmetic-shaped: the ratios in the table directly beneath it were +several times the noise floor the same document quoted. The prose contradicted its own evidence, in adjacent lines, and survived several review passes anyway -- because "within noise" reads as a conclusion rather than as a claim about a measured quantity, so nobody checked it against the number. -The second was deeper. The 2-6% floor had itself been obtained by comparing **two -runs**, which cannot measure a spread at all. Re-running the probe seven times -put the same-configuration spread at 7-61% depending on producer count. So the -floor every "within noise" judgement in the section had been made against was off -by roughly an order of magnitude, and the judgements were not recoverable by +The second was deeper. That floor had itself been obtained by comparing **two +runs**, which cannot measure a spread at all. Seven runs of the same probe put +the same-configuration spread roughly an order of magnitude wider. So the +floor every "within noise" judgement in the section had been made against was +wrong by about that much, and the judgements were not recoverable by adjusting it. +**The figures are deliberately not repeated here.** Both tables live in +[DESIGN-NOTES.md](DESIGN-NOTES.md#d-variance-is-a-finding), which is the one home +for them, and the seven-run sweep's own gap -- its raw runs were never committed +-- is recorded there rather than in a second copy. A rationale that restated the +numbers would be a second surface to keep true by hand, which is the defect this +very section is explaining. + What made the repair possible was already in the probe's output. `reserving_mpsc` and `reserving(32/32)` are the same code at the same layout, measured twice per run, so their ratio is an *empirical* answer to "what does no difference look @@ -562,10 +569,12 @@ measurement instead of to a fact. Its measured span is in the control table in [DESIGN-NOTES.md](DESIGN-NOTES.md#d-variance-is-a-finding), which is where it is recorded rather than here. -**The tempting repair was to invert the claim**, since the seven-run medians put -the re-apportionments at 1.23-1.30x at high producer counts. That would have been +**The tempting repair was to invert the claim**, since at high producer counts +the seven-run medians put the re-apportionments above the same-code control's +own excursions. That would have been the same error with the opposite sign: one host, one microarchitecture, a single -NUMA domain, against a control whose own excursions reach 1.12x. The claim was +NUMA domain, against a control wide enough that the gap is not an ordering. The +claim was withdrawn in both directions instead, and the section now says which configuration is worth measuring locally rather than what the answer is. From 4cfe574083c8192fca7eb2c2736bf3fb2f60e58b Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:45:27 -0400 Subject: [PATCH 128/139] fix(probes): the isolated control was computed in the wrong direction **`isolated.js` divided its control the other way round from its layout ratios, and it moved a reported result.** Every layout ratio is `layout / 32-32`; the control was `32-32 / reserving_mpsc`, so the denominator was the *other* copy. A reciprocal was tabulated as if it were comparable, and the table's whole purpose is reading one column against the other. The direction is not cosmetic because these ranges are not symmetric about 1.00: the reciprocal of `[0.95-1.26]` is `[0.79-1.05]`. Regenerated, `8/56` now sits above its control at 16 producers as well as 32, where the inverted control had shown 32 alone -- a published finding that was wrong, not merely a column that looked odd. The cross-check table in DESIGN-NOTES carried the inverted figures too, and is corrected with the reason recorded beside it. `summarise.js` computed its control correctly throughout -- `reserving / narrowNanos`, denominator `32/32` -- so every drained figure is unaffected, and `summary.txt` still reproduces byte-identically. That the two scripts disagreed is what made this findable at all. **The duplicate-input guard compared spellings.** `run1.txt` and `.\run1.txt` are the same bytes, and a symlink to either is too, while the run count would still report independent captures. Both scripts now compare resolved paths; verified by passing one file under two spellings, which is refused. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-platform-probes/DESIGN-NOTES.md | 25 +++++++++++++------ .../2026-09-16-drained-handshake/isolated.js | 17 ++++++++++--- .../2026-09-16-drained-handshake/isolated.txt | 16 ++++++------ .../2026-09-16-drained-handshake/summarise.js | 8 ++++-- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 96aac07b8..4d4dd75c3 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1066,18 +1066,29 @@ second independent measurement. The two agree on 64/64 at every producer count: | producers | seven-run (above) | three-run capture | same-code control, capture | |---|---|---|---| -| 1 | 1.37x [1.16-1.57] | 1.39x [1.32-1.74] | 1.04x [0.95-1.26] | -| 2 | 1.13x [1.02-1.15] | 1.10x [1.10-1.16] | 1.03x [1.00-1.04] | -| 4 | 1.29x [1.14-1.36] | 1.37x [1.36-1.70] | 0.96x [0.95-1.03] | -| 8 | 1.82x [1.64-2.20] | 1.67x [1.59-2.05] | 0.99x [0.90-1.01] | -| 16 | 3.45x [2.91-4.27] | 4.00x [3.94-4.10] | 0.92x [0.88-1.16] | -| 32 | 3.81x [2.70-4.31] | 4.77x [3.54-4.99] | 0.98x [0.93-1.06] | +| 1 | 1.37x [1.16-1.57] | 1.39x [1.32-1.74] | 0.96x [0.79-1.06] | +| 2 | 1.13x [1.02-1.15] | 1.10x [1.10-1.16] | 0.97x [0.96-1.00] | +| 4 | 1.29x [1.14-1.36] | 1.37x [1.36-1.70] | 1.04x [0.97-1.06] | +| 8 | 1.82x [1.64-2.20] | 1.67x [1.59-2.05] | 1.01x [0.99-1.12] | +| 16 | 3.45x [2.91-4.27] | 4.00x [3.94-4.10] | 1.09x [0.87-1.14] | +| 32 | 3.81x [2.70-4.31] | 4.77x [3.54-4.99] | 1.02x [0.94-1.07] | In the capture, all three runs put 64/64 above the control's whole observed -range at **every** producer count; 16/48 does so at 16 and 32, and 8/56 at 32. +range at **every** producer count; 16/48 and 8/56 each do so at 16 and 32. Three control observations per count is not a band, so this reports what these runs did rather than what a fresh run would do. +**Corrected 2026-09-17: the control column above was inverted when first +published.** `isolated.js` divided the control the other way round -- +`reserving(32/32)` over `reserving_mpsc`, while every layout ratio beside it +divides *by* `reserving(32/32)` -- so a reciprocal was tabulated as though it +were comparable. These ranges are not symmetric about 1.00, so the difference is +real: the reciprocal of `[0.95-1.26]` is `[0.79-1.05]`. It moved a reported +result, not just a column: 8/56 sits above its control at 16 producers as well as +32, where the inverted control had shown 32 alone. `summarise.js` computed its +control in the correct direction throughout, which is why the drained figures are +unaffected. Found by review. + **An earlier reading of the small-count end said "near parity at one or two", and that is withdrawn.** It was restated in six places across the queue crate -- rustdoc, the crate doc, the README twice, `Cargo.toml`, and D-41 -- while the diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index d2ea9a6f7..ccd930e5d 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -40,13 +40,17 @@ if (files.length === 0) { // control range -- derived from one set of bytes. A typo must not make the // capture look more reproducible than it is. { + // Compared by resolved path, not by spelling: `run1.txt` and `./run1.txt` + // are the same bytes, and so is a symlink to either, while the run count + // would still report two independent captures. const seen = new Set(); for (const file of files) { - if (seen.has(file)) { + const real = fs.realpathSync(file); + if (seen.has(real)) { fail(`the same capture was given more than once: ${file}`); process.exit(2); } - seen.add(file); + seen.add(real); } } @@ -120,7 +124,12 @@ const median = (xs) => { const fmt = (x) => x.toFixed(2); const cell = (xs) => `${fmt(median(xs))}x [${fmt(Math.min(...xs))}-${fmt(Math.max(...xs))}]`; -const control = ratios(DEFAULT_LAYOUT, CONTROL_TWIN); +// Same code under two names. The denominator is `DEFAULT_LAYOUT`, exactly as it +// is for every layout ratio above -- a control read against ratios computed the +// other way round is not a control, because these ranges are not symmetric about +// 1.00 and the reciprocal of [0.95-1.26] is [0.79-1.05]. `summarise.js` computes +// its control in this same direction. +const control = ratios(CONTROL_TWIN, DEFAULT_LAYOUT); const measured = new Map(LAYOUTS.map((l) => [l, ratios(l, DEFAULT_LAYOUT)])); // Widths derived from the cells, not fixed. `cell()` renders a median and a @@ -137,7 +146,7 @@ const width = COLUMNS.map((name, column) => const PRODUCERS_WIDTH = Math.max(9, ...COUNTS.map((n) => String(n).length)); out(`isolated regime, ${files.length} run(s): ${files.join(", ")}`); -out(`each layout against ${DEFAULT_LAYOUT}; control is ${DEFAULT_LAYOUT} against ${CONTROL_TWIN}`); +out(`each layout against ${DEFAULT_LAYOUT}; control is ${CONTROL_TWIN} against ${DEFAULT_LAYOUT}`); out("median of the per-run ratios, with the observed range beside it\n"); out( diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt index 6167d4906..ba9089eed 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.txt @@ -1,18 +1,18 @@ isolated regime, 3 run(s): run1.txt, run2.txt, run3.txt -each layout against reserving(32/32); control is reserving(32/32) against reserving_mpsc +each layout against reserving(32/32); control is reserving_mpsc against reserving(32/32) median of the per-run ratios, with the observed range beside it producers control (16/48) (8/56) (64/64) -1 1.04x [0.95-1.26] 0.95x [0.94-1.00] 0.97x [0.95-1.00] 1.39x [1.32-1.74] -2 1.03x [1.00-1.04] 0.96x [0.93-0.97] 0.91x [0.90-0.95] 1.10x [1.10-1.16] -4 0.96x [0.95-1.03] 0.96x [0.86-1.02] 0.96x [0.93-0.99] 1.37x [1.36-1.70] -8 0.99x [0.90-1.01] 0.95x [0.95-1.11] 0.92x [0.88-1.14] 1.67x [1.59-2.05] -16 0.92x [0.88-1.16] 1.25x [1.18-1.29] 1.27x [1.14-1.29] 4.00x [3.94-4.10] -32 0.98x [0.93-1.06] 1.35x [1.34-1.42] 1.43x [1.26-1.45] 4.77x [3.54-4.99] +1 0.96x [0.79-1.06] 0.95x [0.94-1.00] 0.97x [0.95-1.00] 1.39x [1.32-1.74] +2 0.97x [0.96-1.00] 0.96x [0.93-0.97] 0.91x [0.90-0.95] 1.10x [1.10-1.16] +4 1.04x [0.97-1.06] 0.96x [0.86-1.02] 0.96x [0.93-0.99] 1.37x [1.36-1.70] +8 1.01x [0.99-1.12] 0.95x [0.95-1.11] 0.92x [0.88-1.14] 1.67x [1.59-2.05] +16 1.09x [0.87-1.14] 1.25x [1.18-1.29] 1.27x [1.14-1.29] 4.00x [3.94-4.10] +32 1.02x [0.94-1.07] 1.35x [1.34-1.42] 1.43x [1.26-1.45] 4.77x [3.54-4.99] where every run sat above the control's whole observed range: reserving(16/48) 16, 32 producers - reserving(8/56) 32 producers + reserving(8/56) 16, 32 producers reserving(64/64) 1, 2, 4, 8, 16, 32 producers The control's range here is 3 observation(s) per count, which is not diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 76068193d..16b857b1e 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -192,13 +192,17 @@ if (paths.length === 0) { // capture given twice would be certified as two independent runs -- repeated // bytes reported as agreement. { + // Compared by resolved path, not by spelling: `run1.txt` and `./run1.txt` + // are the same bytes, and so is a symlink to either, while the run count + // would still report two independent captures. const seen = new Set(); for (const path of paths) { - if (seen.has(path)) { + const real = fs.realpathSync(path); + if (seen.has(real)) { fail(`the same capture was given more than once: ${path}`); process.exit(2); } - seen.add(path); + seen.add(real); } } const layouts = []; From 33089c61498249f90b8e53f679e48e7bbe47fcc5 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 16 Sep 2026 23:45:27 -0400 Subject: [PATCH 129/139] docs: qualify the layout claim by target, link the derivation, drop a stray figure **The probe index claimed more than the probe measures.** It said `measure` reports "each claim-word apportionment's effect", while the `64/64` row is cfg-elided unless the target has a native 128-bit exchange -- so an i686 or `cmpxchg16b`-less build reports three layouts, not four. The index now says "each claim-word apportionment available on the target" and names the condition. This is the same gap the new CI job covers from the build side. **`isolated.js` was named in the provenance chain without a link**, while the capture README beside it was linked, so a reader could reach the artifact but not the derivation that turns it into the cited figures. **A rationale retyped "thirty-seven seconds"** -- a measured horizon with a home elsewhere -- in a paragraph listing examples of transcriptions that drifted. The example now describes the shape of the error without carrying another copy of the number, which is what the rule in the next section asks for. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-RATIONALE.md | 3 ++- crates/windows-platform-probes/src/lib.rs | 2 +- crates/windows-waitable-queues/src/lib.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DESIGN-RATIONALE.md b/DESIGN-RATIONALE.md index 4983c5d11..3a94ecd34 100644 --- a/DESIGN-RATIONALE.md +++ b/DESIGN-RATIONALE.md @@ -212,7 +212,8 @@ reached, and what was rejected on the way. The evidence was a review history, not an argument. Across the rounds on PR #90, most findings were not wrong measurements -- they were transcriptions that had drifted from the thing they restated: a -table disagreeing with its own copy, a control quoted for the wrong regime, a horizon stated in +table disagreeing with its own copy, a control quoted for the wrong regime, + minutes that the crate's own rate put at thirty-seven seconds. The measurements were fine. The copies were not. diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index 1acfb291f..d73d9fcd7 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -139,7 +139,7 @@ //! | [`doorbell_cost::measure`] | binary only | the absolute cost of `SetEvent`, a set/reset cycle and a satisfied wait against an uncontended atomic, and how much batching drives the doorbell below the push it accompanies | //! | [`doorbell_cost::measure_park_and_wake`] | asserted | that the park-and-wake handshake completes rather than deadlocking, which its first implementation did | //! | [`request_cost::measure`] | binary only | the absolute cost of preparing a path, building an owned `OpenFile`, and duplicating a handle | -//! | [`queue_contention::measure`] | binary only | how the bounded array queue's whole push path scales with producer count, against a contended `fetch_add` floor -- the tail claim is one term in that path, not the whole of it; how `reserving_mpsc` and `slotwise_mpsc` compare end to end in the regime where `reserving_mpsc`'s read of the consumer's position is most expensive (an end-to-end shape comparison -- it neither isolates that read nor bounds it, since the shapes differ in claim protocol and slot metadata too and those differences are not ordered); and each claim-word apportionment's effect on that same whole path -- binary only because it wants more cores than a hosted runner has, takes about a minute, and reports the two shapes as equivalent unless built with `--release` | +//! | [`queue_contention::measure`] | binary only | how the bounded array queue's whole push path scales with producer count, against a contended `fetch_add` floor -- the tail claim is one term in that path, not the whole of it; how `reserving_mpsc` and `slotwise_mpsc` compare end to end in the regime where `reserving_mpsc`'s read of the consumer's position is most expensive (an end-to-end shape comparison -- it neither isolates that read nor bounds it, since the shapes differ in claim protocol and slot metadata too and those differences are not ordered); and each claim-word apportionment available on the target -- the 128-bit `64/64` row is measured only where that exchange is native, so a 32-bit or `cmpxchg16b`-less build reports three -- and its effect on that same whole path -- binary only because it wants more cores than a hosted runner has, takes about a minute, and reports the two shapes as equivalent unless built with `--release` | #![cfg(windows)] #![forbid(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 7a8ba80a5..73e4ab87f 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -153,7 +153,7 @@ //! sweep whose raw runs were never committed, and a three-run capture that was, //! at //! [captures/2026-09-16-drained-handshake/](https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md), -//! whose `isolated.js` derives the +//! whose [isolated.js](https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js) derives the //! per-count layout ratios against a same-code control. The two agree on the //! direction. Until the sweep is re-run with its data kept, the capture is the //! part of this claim a reader can check. From eba2007845b03f10609a44aff329cc0c302f08ac Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 00:01:37 -0400 Subject: [PATCH 130/139] fix(probes): a renamed copy of a capture is not a second observation The duplicate guard added last round compared resolved paths, which catches `./run1.txt` and a symlink but not `copy-of-run1.txt` -- different real path, identical bytes. `paths.length` is both the reported run count and the control band's `(n)`, so a copy narrowed the range and inflated the sample size without adding an observation. Since these artifacts exist to show that a figure reproduces, that is the failure they must not have. Both scripts now deduplicate by SHA-256 of the file contents and name both sides when they refuse. Verified by handing each script `run1.txt` and a byte-identical copy under an unrelated filename: both refuse and exit 2, and both committed outputs still reproduce byte-identically. Twice now the guard has been one notion of identity short -- spelling, then path -- so the check is on the bytes, which is what "independent run" actually means here. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/isolated.js | 19 +++++++++++-------- .../2026-09-16-drained-handshake/summarise.js | 19 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index ccd930e5d..29a584030 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -21,6 +21,7 @@ "use strict"; const fs = require("fs"); +const crypto = require("crypto"); // One writer for the generated artifact, per this repository's output rule: no // formatting site picks a destination, so retargeting the report to a file is a @@ -40,17 +41,19 @@ if (files.length === 0) { // control range -- derived from one set of bytes. A typo must not make the // capture look more reproducible than it is. { - // Compared by resolved path, not by spelling: `run1.txt` and `./run1.txt` - // are the same bytes, and so is a symlink to either, while the run count - // would still report two independent captures. - const seen = new Set(); + // Compared by CONTENT, not by name. A resolved path catches `./run1.txt` and + // a symlink, but not `copy-of-run1.txt`, which has a different real path and + // identical bytes -- and the run count would still present it as a second + // observation, narrowing the reported range without adding data. What is being + // claimed here is independent runs, so identical bytes cannot be two of them. + const seen = new Map(); for (const file of files) { - const real = fs.realpathSync(file); - if (seen.has(real)) { - fail(`the same capture was given more than once: ${file}`); + const digest = crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + if (seen.has(digest)) { + fail(`the same capture was given twice: ${file} is byte-identical to ${seen.get(digest)}`); process.exit(2); } - seen.add(real); + seen.set(digest, file); } } diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 16b857b1e..5ec4f24b5 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -7,6 +7,7 @@ // no single run states because it is a relation between two tables. const fs = require("fs"); +const crypto = require("crypto"); // One writer for the generated artifact, per this repository's output rule: no // formatting site picks a destination, so retargeting the report to a file is a @@ -192,17 +193,19 @@ if (paths.length === 0) { // capture given twice would be certified as two independent runs -- repeated // bytes reported as agreement. { - // Compared by resolved path, not by spelling: `run1.txt` and `./run1.txt` - // are the same bytes, and so is a symlink to either, while the run count - // would still report two independent captures. - const seen = new Set(); + // Compared by CONTENT, not by name. A resolved path catches `./run1.txt` and + // a symlink, but not `copy-of-run1.txt`, which has a different real path and + // identical bytes -- and `paths.length` is both the run count and the control + // band's `(n)`, so a copy would narrow the reported range and inflate the + // sample size without adding an observation. + const seen = new Map(); for (const path of paths) { - const real = fs.realpathSync(path); - if (seen.has(real)) { - fail(`the same capture was given more than once: ${path}`); + const digest = crypto.createHash("sha256").update(fs.readFileSync(path)).digest("hex"); + if (seen.has(digest)) { + fail(`the same capture was given twice: ${path} is byte-identical to ${seen.get(digest)}`); process.exit(2); } - seen.add(real); + seen.set(digest, path); } } const layouts = []; From 58571d76e0ddb7a092d919c4c9ff8ee9e0bd85c5 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 00:01:37 -0400 Subject: [PATCH 131/139] docs: remove three more copies of figures that have a home elsewhere **`5.6x` was in public rustdoc and in the README**, and the rustdoc paragraph carrying it is the one that introduces what is worth saying *without the digits*. It now says "several times" and points at the README's measurement section. **A second capacity passage still had the flat claim.** `lib.rs` said `slotwise_mpsc` reaches 2^62 and `reserving_mpsc` 2^31 -- the shape-not-layout error corrected in the README and in this same file's other capacity paragraph several commits ago. The proposition had a third home I did not find when I swept it. Now per-layout, matching the rest. **The rule's own examples transcribed the horizon table.** They quoted `twenty years`, `5,039 years`, `202 days` and `12.7 days` to illustrate prose overriding its own evidence -- real figures from the probe's table, so a second hand- maintained home inside the rule against second homes. The illustrations keep their force with the magnitudes described rather than copied: what the example demonstrates is a factor being papered over, not its exact size. Also repairs a sentence a multi-line `.Replace` had mangled, leaving "minutes" beginning a line with nothing to attach to. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 10 ++++++---- DESIGN-RATIONALE.md | 5 ++--- crates/windows-waitable-queues/src/lib.rs | 15 ++++++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d7735f317..8fd1dfc8d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1328,11 +1328,13 @@ undecidable. It is decidable, and it is the slash count. The failure does not look like an error, which is why it survives review. It reads as helpfulness: -- A table gives 8/56 twenty years and 64/64 5,039 years at the same rate; the paragraph above it - says 8/56 "reaches the same practical headroom a 128-bit word gives." Nothing is inconsistent — - the prose has simply decided, on the reader's behalf, that a factor of 250 does not matter to +- A table gives one layout a horizon of years and another a horizon hundreds of times longer at the + same rate; the paragraph above it says the shorter one "reaches the same practical headroom a + 128-bit word gives." Nothing is inconsistent — + the prose has simply decided, on the reader's behalf, that a factor of that size does not matter to them. -- A table gives 12/52 202 days; the prose calls it "the first row that is not reachable." +- A table gives a layout a horizon of months; the prose calls it "the first row that is not + reachable." - **Flipping the verdict is not the fix.** Replacing "not reachable" with "reachable by a busy long-lived process" is the same move with the opposite conclusion. Delete the conclusion, do not correct it: *"every row recurs; what changes down the column is how long that takes at a given diff --git a/DESIGN-RATIONALE.md b/DESIGN-RATIONALE.md index 3a94ecd34..08eb9641e 100644 --- a/DESIGN-RATIONALE.md +++ b/DESIGN-RATIONALE.md @@ -212,9 +212,8 @@ reached, and what was rejected on the way. The evidence was a review history, not an argument. Across the rounds on PR #90, most findings were not wrong measurements -- they were transcriptions that had drifted from the thing they restated: a -table disagreeing with its own copy, a control quoted for the wrong regime, - -minutes that the crate's own rate put at thirty-seven seconds. The measurements were fine. The +table disagreeing with its own copy, a control quoted for the wrong regime, a wrap horizon stated in +minutes that the crate's own published rate contradicts. The measurements were fine. The copies were not. Two weaker rules were considered and rejected. **"Keep the copies in sync"** is what had already diff --git a/crates/windows-waitable-queues/src/lib.rs b/crates/windows-waitable-queues/src/lib.rs index 73e4ab87f..d7ac006c4 100644 --- a/crates/windows-waitable-queues/src/lib.rs +++ b/crates/windows-waitable-queues/src/lib.rs @@ -379,12 +379,17 @@ //! //! **What moves these numbers.** Producer count, how hard the consumer drains, //! and where the threads are scheduled -- placement alone moved an SPSC handoff -//! by 5.6x on an earlier host this workspace measured. -//! -//! Two things that look like reasons to choose and are not. **Capacity**: on a -//! 64-bit target `slotwise_mpsc` reaches 2^62 slots and `reserving_mpsc` 2^31. +//! by several times on an earlier host this workspace measured; the figure is in +//! the README's measurement section rather than repeated here. +//! +//! Two things that look like reasons to choose and are not. **Capacity**: the +//! ceiling is the layout's, not the shape's. On a 64-bit target `slotwise_mpsc` +//! reaches 2^62 slots, and `reserving_mpsc` reaches 2^31 under `Balanced`, 2^47 +//! under `Enduring`, 2^55 under `Perpetual` and 2^62 under `Wide` -- the last +//! being the crate-wide ceiling, so under `Wide` the two shapes reach the same +//! number. //! On a 32-bit one the crate-wide ceiling is 2^30 and **both** shapes land -//! there -- `reserving_mpsc`'s packed 2^31 is clamped down to it too -- so the +//! there -- every `reserving_mpsc` layout is clamped down to it too -- so the //! difference disappears entirely and the comparison means nothing at all. //! Either way it counts slots allocated up front rather than items ever pushed, //! and 2^31 slots is tens of gigabytes before the ring holds anything useful. From e7e0ebe852dc6f2e65cfeba8a9b9bc08babef5dd Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 00:17:57 -0400 Subject: [PATCH 132/139] fix(probes): refuse a capture whose runs were not taken together, and render Wide **Nothing checked that a capture's runs describe one configuration.** Both scripts aggregated by table contents alone, so a debug build, or a run from another machine, could be passed alongside the release runs and produce a median and a control span describing no configuration that was ever measured -- while the README presents the result as one capture. Content hashing catches a *repeated* run; it says nothing about a *wrong* one. Each run prints its own `host:`, `profile:` and `sampling:` lines. Both scripts now require those to agree across their inputs and name both sides when they do not. Verified by mixing a `profile: debug` run and an `aarch64` run into the committed release set: each is refused, naming the disagreeing field. **The corpus never rendered a measured `64/64` row.** `Wide` is cfg-gated behind a native 128-bit exchange, so on every target where it is enabled the renderer's 64/64 columns were exercised only by the did-not-run path -- a width or alignment fault in a real Wide measurement would have gone unseen by the suite that exists to catch exactly that. The ordinary case now carries measured Wide rows in both regimes. Both committed outputs still reproduce byte-identically. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/isolated.js | 35 ++++++++++++++++ .../2026-09-16-drained-handshake/summarise.js | 37 ++++++++++++++++- .../src/bin/queue_contention/corpus.json | 41 ++++++++++++++++++- 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 29a584030..4c3f4109d 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -57,6 +57,38 @@ if (files.length === 0) { } } + +// The attribution a run prints about itself. A capture is runs of one build on +// one host under one sampling regime, so these must agree across the inputs -- +// mixing a debug run, or another machine's, yields a median and a control span +// that describe no configuration that was ever measured, and nothing else here +// would notice. +function attribution(text, where) { + const line = (prefix) => { + const found = text.split(/\r?\n/).find((l) => l.startsWith(prefix)); + if (found === undefined) { + fail(`${where}: no "${prefix}" line; this does not look like a probe report`); + process.exit(2); + } + return found.trim(); + }; + return [line("host:"), line("profile:"), line("sampling:")].join(" | "); +} + +function requireOneConfiguration(entries) { + const first = entries[0]; + for (const entry of entries.slice(1)) { + if (entry.attribution !== first.attribution) { + fail( + `${entry.name} was taken under different conditions from ${first.name}:\n` + + ` ${first.name}: ${first.attribution}\n` + + ` ${entry.name}: ${entry.attribution}`, + ); + process.exit(2); + } + } +} + const COUNTS = [1, 2, 4, 8, 16, 32]; const DEFAULT_LAYOUT = "reserving(32/32)"; // Same code as DEFAULT_LAYOUT, under the shipping type's own name. @@ -89,6 +121,9 @@ function isolatedRows(file) { return rows; } +requireOneConfiguration( + files.map((f) => ({ name: f, attribution: attribution(fs.readFileSync(f, "utf8"), f) })), +); const tables = files.map(isolatedRows); function ratios(numerator, denominator) { diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 5ec4f24b5..f34fa5e9a 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -82,7 +82,7 @@ function wholeCount(text, what) { // A ratio triple must be ordered and must contain its own point estimate. // `positive` accepts each number on its own, so `2.00x [3.00-1.00]` passes -// three separate checks and is still not a interval any instrument produced. +// three separate checks and is still not an interval any instrument produced. function orderedTriple(point, low, high, where) { if (point === null || low === null || high === null) return false; if (low > high) { @@ -208,8 +208,43 @@ if (paths.length === 0) { seen.set(digest, path); } } + +// The attribution a run prints about itself. A capture is runs of one build on +// one host under one sampling regime, so these must agree across the inputs -- +// mixing a debug run, or another machine's, yields a median and a control span +// that describe no configuration that was ever measured, and nothing else here +// would notice. +function attribution(text, where) { + const line = (prefix) => { + const found = text.split(/\r?\n/).find((l) => l.startsWith(prefix)); + if (found === undefined) { + fail(`${where}: no "${prefix}" line; this does not look like a probe report`); + process.exit(2); + } + return found.trim(); + }; + return [line("host:"), line("profile:"), line("sampling:")].join(" | "); +} + +function requireOneConfiguration(entries) { + const first = entries[0]; + for (const entry of entries.slice(1)) { + if (entry.attribution !== first.attribution) { + fail( + `${entry.name} was taken under different conditions from ${first.name}:\n` + + ` ${first.name}: ${first.attribution}\n` + + ` ${entry.name}: ${entry.attribution}`, + ); + process.exit(2); + } + } +} + const layouts = []; const controls = []; +requireOneConfiguration( + paths.map((p) => ({ name: p, attribution: attribution(fs.readFileSync(p, "utf8"), p) })), +); for (const path of paths) { const lines = fs.readFileSync(path, "utf8").split(/\r?\n/); const layout = drainedLayout(lines, path); diff --git a/crates/windows-platform-probes/src/bin/queue_contention/corpus.json b/crates/windows-platform-probes/src/bin/queue_contention/corpus.json index 6239776e4..f3e312f12 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/corpus.json +++ b/crates/windows-platform-probes/src/bin/queue_contention/corpus.json @@ -25,7 +25,7 @@ "cases": [ { "name": "ordinary", - "why": "A plausible run. Establishes that the tables render and line up at all, so the stressed cases below are testing something.", + "why": "A plausible run. Establishes that the tables render and line up at all, so the stressed cases below are testing something. It also carries measured `reserving(64/64)` rows in both regimes: that layout is cfg-gated behind a native 128-bit exchange, so without them the renderer's 64/64 columns were only ever exercised by the did-not-run path, and a width or alignment fault in a real Wide measurement would have rendered unseen.", "observation": { "available_parallelism": 8, "isolated": [ @@ -154,6 +154,24 @@ 0, 23.8, 36.5 + ], + [ + "reserving(64/64)", + 1, + 7.5, + 133333333, + 0, + 7.1, + 7.9 + ], + [ + "reserving(64/64)", + 8, + 61.7, + 16207455, + 0, + 54.1, + 88.1 ] ], "drained": [ @@ -264,6 +282,24 @@ 1160, 60.5, 70.8 + ], + [ + "reserving(64/64)", + 1, + 29.3, + 34129692, + 12, + 27.4, + 31 + ], + [ + "reserving(64/64)", + 8, + 103.3, + 9680542, + 480, + 92.1, + 118.4 ] ] }, @@ -275,7 +311,8 @@ "ns/op range": 2 }, "contains": [ - "processors available to this process: 8" + "processors available to this process: 8", + "reserving(64/64)" ], "absent": [] } From 58ddaf94c59999632c93129275c56c288ab7e137 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 00:17:58 -0400 Subject: [PATCH 133/139] docs: correct a stale rule count, and three more transcribed figures The restatement section said "Three rules follow" after this branch added rules 4 and 5, so a reader was told the wrong number by the document that exists to catch exactly that kind of drift. Three more figures with a home elsewhere are now described rather than copied: the `57 of 61` tally, and `12.7 days` / `202 days` in the rule's own worked example. That example is the recommended *good* form, so a reader following it was being shown the transcription the surrounding rule forbids. **And a ranking is now marked as historical.** A design note said one fact "is the worst, restated in more places than any of the others" -- a measured ordering with no committed census, dropped of its digits but not of the citation obligation, which the rule directly below says is not how that works. It now says when the count was taken and that none is committed. Also `a interval` -> `an interval`. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 6 +++--- DESIGN-NOTES.md | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8fd1dfc8d..f35201ad6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1181,7 +1181,7 @@ hypothetical: across three consecutive review rounds on PR #42, *five of six fin corrections that had not propagated* rather than original defects. See [DESIGN-NOTES.md](../DESIGN-NOTES.md) -> [Restatement drift](../DESIGN-NOTES.md#restatement-drift). -Three rules follow, ordered by how little each depends on anyone remembering. +Five rules follow, ordered by how little each depends on anyone remembering. ### 1. Prefer a derived fact to a restated one @@ -1298,7 +1298,7 @@ sites in three wordings. document is not a finding; it is a hand-computed copy of one, checked by nobody and stale the moment any input moves. The counts are the finding. This rule was earned: an instructions file in this repository claimed "in both cases roughly 60%" about two figures given four words earlier, - one of which was 57 of 61. + neither of which rounded to it. - **The same applies to incidental tallies** — test counts, file counts, line counts. If the number is not itself the finding, leave it out; "the gate is green" says what "308 lib tests" pretends to. @@ -1338,7 +1338,7 @@ The failure does not look like an error, which is why it survives review. It rea - **Flipping the verdict is not the fix.** Replacing "not reachable" with "reachable by a busy long-lived process" is the same move with the opposite conclusion. Delete the conclusion, do not correct it: *"every row recurs; what changes down the column is how long that takes at a given - rate — 16/48 at 12.7 days against 12/52 at 202 days."* + rate — the table's own figures say how much."* Three words are the usual tell, and each is a conclusion wearing a measurement's clothes: **practical**, **effectively**, **reachable**. So are "enough", "negligible", "safe to", and any diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 17646dae9..298c3cb7d 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1881,7 +1881,9 @@ figures. The thing to watch is that in `windows-waitable-queues`, a handful of single facts -- `Perpetual`'s reservation-count ceiling, `Balanced`'s recurrence horizon, `Perpetual`'s position span, `Balanced`'s field ceiling -- are each restated many times across several files, by hand, with nothing checking -any of them. The ceiling is the worst, restated in more places than any of the others. +any of them. Which of them has the most copies was counted once, during the review rounds that +produced this section, and has not been counted since; no census is committed, so that ordering is +recorded here as a historical observation rather than a current fact. **The exact counts are deliberately not recorded here.** An earlier version of this section carried them as a table, and the table drifted within days: one row gained an occurrence when a qualifier was From cf853283edbd339a2869506cb916cbcc7df0ab21 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 00:31:28 -0400 Subject: [PATCH 134/139] fix(probes): read capture tables to their terminator, and name the provenance gap **A fixed six-row slice discarded a seventh row without reporting it.** Both parsers read exactly six lines after the marker, so a capture carrying an extra or duplicated producer row past the sixth satisfied `EXPECTED_PRODUCERS` on the truncated map and was certified while its data was dropped. Every guard added to these scripts -- duplicate keys, the expected producer set, content hashing -- ran on rows the slice had already thrown away. Both now read to the blank line that ends the table, which puts every row in front of those checks. Verified by inserting a seventh row: previously silent, now reported twice over, as an unexpected producer set and as a layout row with no comparison row. **The attribution check cannot see the instrument, and now says so.** It compares `host:`, `profile:` and `sampling:` -- the only provenance a report carries. Two runs from *different probe commits* on one machine under one profile therefore agree, and this is the capture where that matters most: `M4.3` changed the drained procedure, so a pre-handshake and a post-handshake run would pass and have their medians combined as though one procedure produced both. The instrument commit lives in the capture README, which is an assertion by whoever took the capture rather than something anything verifies. Closing it means the probe stamping its own build identity into the report, which is a build script this crate does not have -- so it is queued as `M4.8` rather than described in a comment and forgotten, with `windows-placement-probe`'s `build_identity` named as the worked example. Both scripts state the limit at the point where a reader would otherwise assume it was covered. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 21 ++++++++++++++ .../2026-09-16-drained-handshake/isolated.js | 9 ++++++ .../2026-09-16-drained-handshake/summarise.js | 29 +++++++++++++++++-- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 4968ed61f..bca71d524 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -237,6 +237,27 @@ correctness in the archive. them. Until this lands, a shape that needs two dimensions must be added by hand, which is exactly the imagination-driven process M2.12 exists to replace. +- [ ] **M4.8** -- Have the queue-contention report carry the build identity that produced it, and + have the capture scripts require it to agree. + + **Gap:** a run's report states its `host:`, `profile:` and `sampling:`, and the capture scripts + now refuse a set whose runs disagree on any of those. None of it identifies the *instrument*. Two + runs from different probe commits, on one machine, under one profile, pass that check -- and this + is the capture where that matters most, because `M4.3` changed the drained procedure, so a + pre-handshake and a post-handshake run would have their medians combined as though one procedure + produced both. The instrument commit is recorded in the capture README, which is an assertion by + whoever took the capture rather than something anything verifies. + + **Target:** `windows-placement-probe`'s `build_identity` module is the worked example -- a build + script stamps the commit, the dirty flag and the build source into env vars that the binary reads + at run time, and `BuildIdentity::current()` renders them. `windows-platform-probes` has no build + script today, so this adds one. The report prints the identity beside the existing attribution + lines, `requireOneConfiguration` in both capture scripts includes it, and the sabotage is two runs + of different commits being refused. + + **Blocker recorded when queued:** none. The dependency exists next door and is already proven by + that crate's own tests. + ## M5 -- Carried over from M2: unblocked hygiene diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 4c3f4109d..1cd985ee2 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -76,6 +76,15 @@ function attribution(text, where) { } function requireOneConfiguration(entries) { + // **What this cannot check.** The report carries no build identity, so two + // runs of DIFFERENT probe commits on one host under one profile agree here. + // That matters most for exactly this capture: `M4.3` changed the drained + // procedure, so a pre-handshake and a post-handshake run would pass and have + // their medians combined as though one procedure produced both. The + // instrument commit is asserted by the capture README, which is a claim by + // the person who took the capture rather than something these scripts verify. + // Closing it needs the probe to stamp its own build identity into the report; + // `M4.8` in CHECKLIST.md owns that. const first = entries[0]; for (const entry of entries.slice(1)) { if (entry.attribution !== first.attribution) { diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index f34fa5e9a..edd121136 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -96,6 +96,22 @@ function orderedTriple(point, low, high, where) { return true; } +// A table's rows, from `from` to the blank line that ends it. +// +// A fixed six-row slice discards a seventh row without reporting it, so a +// capture carrying an extra or duplicated producer row past the sixth would +// satisfy `EXPECTED_PRODUCERS` on the truncated map and be certified while its +// data was silently dropped. Reading to the terminator puts every row in front +// of the duplicate and expected-set checks instead. +function tableBody(lines, from) { + const body = []; + for (let i = from; i < lines.length; i += 1) { + if (lines[i].trim() === "") break; + body.push(lines[i]); + } + return body; +} + // producers -> { narrowNanos, ratios: [16/48, 8/56, 64/64] } function drainedLayout(lines, path) { let start = -1; @@ -111,7 +127,7 @@ function drainedLayout(lines, path) { return new Map(); } const rows = new Map(); - for (const line of lines.slice(start + 2, start + 8)) { + for (const line of tableBody(lines, start + 2)) { const fields = line.trim().split(/\s+/); const producers = wholeCount(fields[0], `${path}: a drained layout producer count`); if (producers === null) continue; @@ -157,7 +173,7 @@ function drainedLayout(lines, path) { function drainedComparison(lines, path) { const start = lines.findIndex((line) => line.includes("reserving/slotwise")); const rows = new Map(); - for (const line of lines.slice(start + 2, start + 8)) { + for (const line of tableBody(lines, start + 2)) { const fields = line.trim().split(/\s+/); const producers = wholeCount(fields[0], `${path}: a comparison producer count`); if (producers === null) continue; @@ -227,6 +243,15 @@ function attribution(text, where) { } function requireOneConfiguration(entries) { + // **What this cannot check.** The report carries no build identity, so two + // runs of DIFFERENT probe commits on one host under one profile agree here. + // That matters most for exactly this capture: `M4.3` changed the drained + // procedure, so a pre-handshake and a post-handshake run would pass and have + // their medians combined as though one procedure produced both. The + // instrument commit is asserted by the capture README, which is a claim by + // the person who took the capture rather than something these scripts verify. + // Closing it needs the probe to stamp its own build identity into the report; + // `M4.8` in CHECKLIST.md owns that. const first = entries[0]; for (const entry of entries.slice(1)) { if (entry.attribution !== first.attribution) { From 6c6551ad7636261ceffc45eb292ffe81c522206d Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 00:44:45 -0400 Subject: [PATCH 135/139] fix(probes): require a release capture, and check the half of it nobody checked Four ways a capture could still be certified while being something other than it claimed. All four sit in the validation added over the last several rounds, which had grown thorough about the rows it saw and silent about the rest. **Agreement is not measurement.** The attribution check required `host:`, `profile:` and `sampling:` to agree across runs -- and three *debug* reports agree with each other. The probe stamps a debug run "NOT A MEASUREMENT" precisely because its figures are not one, so the check would have certified a capture that is internally consistent and meaningless. `profile: release` is now required outright, not merely matched. **`processors available to this process` is part of the configuration.** An affinity mask changes how many processors a run may use while leaving the host banner identical, and producer counts are read against that number. Now included in the compared attribution. **Only the layout table's producer set was checked.** The loop that derives the control iterates the *layout's* keys, so an extra row in the comparison table was accepted into its map and then never read -- half the capture able to carry rows nothing reported. The comparison table now gets the same expected-set check, and `isolated.js`, which had no such check at all, now requires every shape it reads to cover exactly the swept counts. **An unanchored ratio pattern matched a suffix.** `1.2.3x [1.0-2.0]` yielded `2.3x [1.0-2.0]`, which passed every downstream check as an ordinary cell. Anchored to whitespace at both ends, so a malformed cell now fails the three-ratios count instead of being silently repaired into a plausible one. Each verified by sabotage against the committed runs: an all-debug set, a run with a different affinity, an extra comparison row, and a `1.2.3x` cell are all refused, each naming what it found. Both committed outputs still reproduce byte-identically. Also corrects a doc comment that claimed `include_str!` makes malformed JSON a build failure. It makes a *missing* corpus one; malformed JSON fails when the test parses it, which is a different failure mode and worth distinguishing in a file about distinguishing failure modes. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-09-16-drained-handshake/isolated.js | 32 ++++++++++++++- .../2026-09-16-drained-handshake/summarise.js | 41 ++++++++++++++++--- .../src/bin/queue_contention/tests.rs | 6 ++- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 1cd985ee2..7d3c1fc7a 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -72,7 +72,21 @@ function attribution(text, where) { } return found.trim(); }; - return [line("host:"), line("profile:"), line("sampling:")].join(" | "); + const profile = line("profile:"); + // Agreement is not enough on its own: three debug reports agree with each + // other, and the probe stamps a debug run "NOT A MEASUREMENT" precisely + // because its figures are not one. A capture built from them would be + // internally consistent and meaningless. + if (profile !== "profile: release") { + fail(`${where}: ${profile} -- only a release run is a measurement`); + process.exit(2); + } + // Included because it can differ while the host banner does not: an affinity + // mask changes how many processors the process may use without changing the + // machine it names, and producer counts are read against that number. + return [line("host:"), profile, line("sampling:"), line("processors available to this process:")].join( + " | ", + ); } function requireOneConfiguration(entries) { @@ -127,6 +141,22 @@ function isolatedRows(file) { rows.set(key, Number(m[3])); } } + // Every shape this script reads must appear at exactly the swept producer + // counts, and nothing else. `ratios()` looks up only the counts in `COUNTS`, + // so an extra row -- a newly added 64-producer sweep, say -- would sit in the + // map unread while the summary reported success over a capture it had only + // partly used. + for (const shape of [DEFAULT_LAYOUT, CONTROL_TWIN, ...LAYOUTS]) { + const seen = [...rows.keys()] + .filter((k) => k.startsWith(`${shape}@`)) + .map((k) => Number(k.slice(shape.length + 1))) + .sort((a, b) => a - b); + if (seen.length !== 0 && seen.join(",") !== COUNTS.join(",")) { + throw new Error( + `${file}: ${shape} covers producers [${seen}], expected [${COUNTS}]`, + ); + } + } return rows; } diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index edd121136..1110508fa 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -17,11 +17,11 @@ let sink = (text) => process.stdout.write(text + "\n"); const out = (text = "") => sink(text); const fail = (text) => process.stderr.write(text + "\n"); -// A ratio cell, with all three numbers required to be plain decimals. The -// looser `[0-9.]+` also matched a run of dots, so a malformed `...x [..-..]` -// parsed and `Number("...")` became NaN -- which compares false against every -// guard downstream and would surface in the output rather than be rejected. -const RATIO = /(\d+\.\d+)x \[(\d+\.\d+)-(\d+\.\d+)\]/g; +// A ratio cell, with all three numbers required to be plain decimals, and the +// whole cell required to stand alone between whitespace. Unanchored, the pattern +// matches a SUFFIX of malformed text -- `1.2.3x [1.0-2.0]` yields `2.3x +// [1.0-2.0]`, which then passes every downstream check as an ordinary cell. +const RATIO = /(?<=^|\s)(\d+\.\d+)x \[(\d+\.\d+)-(\d+\.\d+)\](?=\s|$)/g; function median(values) { const sorted = [...values].sort((a, b) => a - b); @@ -239,7 +239,21 @@ function attribution(text, where) { } return found.trim(); }; - return [line("host:"), line("profile:"), line("sampling:")].join(" | "); + const profile = line("profile:"); + // Agreement is not enough on its own: three debug reports agree with each + // other, and the probe stamps a debug run "NOT A MEASUREMENT" precisely + // because its figures are not one. A capture built from them would be + // internally consistent and meaningless. + if (profile !== "profile: release") { + fail(`${where}: ${profile} -- only a release run is a measurement`); + process.exit(2); + } + // Included because it can differ while the host banner does not: an affinity + // mask changes how many processors the process may use without changing the + // machine it names, and producer counts are read against that number. + return [line("host:"), profile, line("sampling:"), line("processors available to this process:")].join( + " | ", + ); } function requireOneConfiguration(entries) { @@ -266,6 +280,7 @@ function requireOneConfiguration(entries) { } const layouts = []; +const comparisons = []; const controls = []; requireOneConfiguration( paths.map((p) => ({ name: p, attribution: attribution(fs.readFileSync(p, "utf8"), p) })), @@ -275,6 +290,7 @@ for (const path of paths) { const layout = drainedLayout(lines, path); const comparison = drainedComparison(lines, path); layouts.push(layout); + comparisons.push(comparison); // The same code measured twice in one run: `reserving_mpsc` in the comparison // table against `32/32` in the layout table. const control = new Map(); @@ -308,6 +324,19 @@ layouts.forEach((layout, i) => { } }); +// The comparison table needs the same check, and for the same reason: the loop +// that builds the control iterates the LAYOUT's keys, so an extra comparison row +// is accepted into its map and then never looked at. Only the layout side was +// checked, which left half the capture able to carry rows nothing reported. +comparisons.forEach((comparison, i) => { + const seen = [...comparison.keys()].sort((a, b) => a - b); + if (seen.join(",") !== EXPECTED_PRODUCERS.join(",")) { + problems.push( + `${paths[i]}: drained comparison covers producers [${seen}], expected [${EXPECTED_PRODUCERS}]`, + ); + } +}); + const producers = EXPECTED_PRODUCERS; out(`runs: ${paths.length}`); diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs index 5bd8c5d0c..69112b3af 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -42,8 +42,10 @@ fn assert_aligned(name: &str, lines: &[&str], why: &str) { } } -/// Compiled in, so a missing or malformed corpus is a build failure rather than -/// a test that silently runs nothing. +/// Compiled in, so a missing corpus is a build failure rather than a test that +/// silently runs nothing. Malformed JSON is caught a step later, when the corpus +/// is parsed: that is a test failure, not a build failure, because +/// `include_str!` checks only that the bytes exist. const CORPUS: &str = include_str!("corpus.json"); /// A row is `[shape, producers, nanos_per_op, ops_per_second, refusals, From 1fb482f318f44ea5ef7339770b1a7bc2815c2b11 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 01:01:14 -0400 Subject: [PATCH 136/139] fix(probes): reject an unrecognised layout, and correct a note that retracted itself **A design note stated the withdrawn conclusion one paragraph above withdrawing it.** It said the `u64` re-apportionments "do not [fall outside the same-code control band], at any producer count", while the next paragraph withdraws the apportionment claim on the grounds that they *do* sit outside it at sixteen and thirty-two producers -- too close to establish an ordering, which is a different statement from not being outside at all. `D-41` and the committed capture both say the latter. Corrected to match them. **An unrecognised claim-word layout was parsed and never read.** `isolated.js` checked that each layout it knows covers the swept producer counts, but said nothing about a layout it does not know: a newly added apportionment would sit in the map unread while the summary reported success over a capture it had only partly used. The first attempt at that check rejected every unknown *shape* and refused the committed capture on its first row -- the isolated table legitimately carries `baseline_fetch_add`, `slotwise_mpsc` and `permit_mpsc`, which this script does not derive from and which are not anomalies. Caught by running it, and the reason is now recorded at the check. Scoped to `reserving(...)` rows; verified by renaming one to an unknown apportionment, which is refused. **The content-hash duplicate check states its assumption.** Review is right that the report carries no per-invocation identifier, so "identical bytes" stands in for "same run", and two genuinely independent runs could in principle be refused. Kept, because that refusal is loud and diagnosable while accepting a duplicated run silently fabricates agreement -- but the assumption is written at the check, with `M4.8` named as what replaces the heuristic once the report carries a real identity. Queues **`M4.9`**: review's point that the prose-volume section keeps its history in Tier 1 is correct and accepted. Moving it is a large enough edit to deserve its own commit rather than riding along with a review round. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 16 ++++++++++ .../windows-platform-probes/DESIGN-NOTES.md | 5 ++- .../2026-09-16-drained-handshake/isolated.js | 32 ++++++++++++++++--- .../2026-09-16-drained-handshake/summarise.js | 9 ++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index bca71d524..c86b6107b 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -237,6 +237,22 @@ correctness in the archive. them. Until this lands, a shape that needs two dimensions must be added by hand, which is exactly the imagination-driven process M2.12 exists to replace. +- [ ] **M4.9** -- Move the prose-volume section's history out of Tier 1. + + **Gap:** `DESIGN-NOTES.md`'s "Prose volume is not the error surface" section carries its motivating + question, the alternatives rejected, the review-history it was measured from, an earlier draft it + corrects, and an undecided mechanism. Tier 1 is for the current decision; Tier 2 + ([DESIGN-RATIONALE.md](../../DESIGN-RATIONALE.md)) already holds the history for the neighbouring + one-home rule, so this section splits its own rationale across both tiers and makes the decision + harder to find inside it. + + **Target:** a compact decision in `DESIGN-NOTES.md` -- what is watched, and why restatement count + rather than volume -- with the narrative moved to `DESIGN-RATIONALE.md` beside the rule it belongs + with, per the three-tier convention. No content is dropped; it changes which file owns it. + + **Blocker recorded when queued:** none. Raised by review during PR #90 and accepted; deferred only + because a move of this size is safer as its own commit than appended to a review round. + - [ ] **M4.8** -- Have the queue-contention report carry the build identity that produced it, and have the capture scripts require it to agree. diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 4d4dd75c3..70356fb77 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1136,7 +1136,10 @@ data kept. **Widening the word is the one effect this probe establishes.** At sixteen and thirty-two producers the isolated 128-bit rows fall outside the same-code -control band; the `u64` re-apportionments do not, at any producer count. +control band by a wide margin. The `u64` re-apportionments also sit outside it at +those counts, but too close to it to establish an ordering or a cost -- which is +the distinction the next paragraph withdraws the apportionment claim over, and +stating it as "they do not" contradicted both that paragraph and the capture. That is a real effect on this machine, and its direction is mechanically unsurprising -- `cmpxchg16b` against `lock cmpxchg`. Whether it reproduces on another microarchitecture is a question for the probe, not for this note. diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js index 7d3c1fc7a..1c523a219 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/isolated.js @@ -46,6 +46,15 @@ if (files.length === 0) { // identical bytes -- and the run count would still present it as a second // observation, narrowing the reported range without adding data. What is being // claimed here is independent runs, so identical bytes cannot be two of them. + // + // **This is a heuristic, and its assumption is worth stating.** The report + // carries no per-invocation identifier, so "identical bytes" stands in for + // "same run". Two genuinely independent runs producing identical bytes would + // be refused -- possible in principle, since the figures are rounded, and + // vanishingly unlikely across this many of them. The refusal is loud and + // diagnosable; accepting a duplicated run would silently fabricate agreement, + // which is the worse of the two. `M4.8` replaces the heuristic with a real + // identity once the report carries one. const seen = new Map(); for (const file of files) { const digest = crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); @@ -142,11 +151,24 @@ function isolatedRows(file) { } } // Every shape this script reads must appear at exactly the swept producer - // counts, and nothing else. `ratios()` looks up only the counts in `COUNTS`, - // so an extra row -- a newly added 64-producer sweep, say -- would sit in the - // map unread while the summary reported success over a capture it had only - // partly used. - for (const shape of [DEFAULT_LAYOUT, CONTROL_TWIN, ...LAYOUTS]) { + // counts, and no *layout* it does not read may appear at all. `ratios()` looks + // up only the counts in `COUNTS` for only the shapes it knows, so both an + // extra count and an unrecognised layout would sit in the map unread while the + // summary reported success over a capture it had only partly used. + // + // Scoped to `reserving(...)` rows deliberately: the isolated table also + // carries `baseline_fetch_add`, `slotwise_mpsc` and `permit_mpsc`, which this + // script does not derive from and which are not anomalies. An earlier version + // rejected every unknown shape and refused the committed capture on its first + // row. + const known = [DEFAULT_LAYOUT, CONTROL_TWIN, ...LAYOUTS]; + for (const key of rows.keys()) { + const shape = key.slice(0, key.lastIndexOf("@")); + if (shape.startsWith("reserving(") && !known.includes(shape)) { + throw new Error(`${file}: unexpected claim-word layout ${shape}`); + } + } + for (const shape of known) { const seen = [...rows.keys()] .filter((k) => k.startsWith(`${shape}@`)) .map((k) => Number(k.slice(shape.length + 1))) diff --git a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js index 1110508fa..ddbc3baca 100644 --- a/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js +++ b/crates/windows-platform-probes/captures/2026-09-16-drained-handshake/summarise.js @@ -214,6 +214,15 @@ if (paths.length === 0) { // identical bytes -- and `paths.length` is both the run count and the control // band's `(n)`, so a copy would narrow the reported range and inflate the // sample size without adding an observation. + // + // **This is a heuristic, and its assumption is worth stating.** The report + // carries no per-invocation identifier, so "identical bytes" stands in for + // "same run". Two genuinely independent runs producing identical bytes would + // be refused -- possible in principle, since the figures are rounded, and + // vanishingly unlikely across this many of them. The refusal is loud and + // diagnosable; accepting a duplicated run would silently fabricate agreement, + // which is the worse of the two. `M4.8` replaces the heuristic with a real + // identity once the report carries one. const seen = new Map(); for (const path of paths) { const digest = crypto.createHash("sha256").update(fs.readFileSync(path)).digest("hex"); From d17e78aa360fe59e53ecd54128b0cd3ab1a8495c Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 01:21:35 -0400 Subject: [PATCH 137/139] docs: move the prose-volume history to Tier 2, and correct four citations **Tier 1 was carrying its own history**, which review raised twice and I deferred once with "no blocker" -- a deferral the rule does not allow, since the only legitimate reason is a genuine blocking factor and "safer as its own commit" is not one. Done here instead. `DESIGN-NOTES.md` now states the decision: what is watched and why restatement count rather than volume; prose carries the claim and an artifact carries the number; formal specification and restatement control address different classes; the cut is to restated assertions, never to rationale. The motivating question, the census procedure and its rejected table, the finding taxonomy, the superseded drafts, the undecided mechanism and the costed-but-unadopted remedy move to `DESIGN-RATIONALE.md` beside the one-home rule they belong with. Nothing is dropped; what changes is which file owns it. `M4.9`, queued last round for this, is removed rather than left checked against work now done in place. **Four citations pointed at the wrong decision.** Three named `D-40` as the queue crate's "promote the load" answer -- the archive's `M4.3` entry, the implementation comment in `queue_contention.rs`, and by extension anyone who followed either. `D-40` is about *atomicity*, and says so in its first sentence while pointing at `D-38` for ordering; `D-38` is the promotion rule. The fourth attributed the i686 support decision to `D-40` as well, where `D-18` is the decision whose retained cost analysis covers that target. Verified by reading all three decision rows rather than by recognising the number: `D-38` states the one-atomic-one-discipline rule, `D-40` opens by citing `D-38`, and `D-18` is the 128-bit-refusal analysis kept for exactly this reason. Also links the cross-reference in `DESIGN-RATIONALE.md` at the section that actually defines the one-home rule, and makes `corpus.json` a clickable link in the rustdoc that tells contributors to edit it. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESIGN-NOTES.md | 217 +++--------------- DESIGN-RATIONALE.md | 212 ++++++++++++++++- crates/windows-platform-probes/CHECKLIST.md | 16 -- .../COMPLETED-CHECKLIST.md | 2 +- crates/windows-platform-probes/Cargo.toml | 2 +- .../src/bin/queue_contention/tests.rs | 2 +- .../src/queue_contention.rs | 2 +- 7 files changed, 246 insertions(+), 207 deletions(-) diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 298c3cb7d..bcb098811 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1859,150 +1859,14 @@ written. The rule is about **discarded failure information**, not about discarde The audit this decision implies is queued as [CHECKLIST.md](CHECKLIST.md) -> `M22.1`; it is not scheduled by this note alone. - ## Prose volume is not the error surface; restatement count is -[Restatement drift](#restatement-drift) explains the mechanism and gives the remedy. This note -records something that section does not: a measurement of **where** the drift actually lives, taken -after PR #90's eighteenth review round, and what follows from it about formal specification. - -The question that prompted it was whether this repository simply says too much -- whether English, -which must be inexact to serve human readers, is being asked to carry a specification load it cannot -bear, and whether some formal specification plus substantially less prose would shrink the error -surface. - -### The measurement, and why it is not written down here - -Prose volume was looked at first and set aside: whatever the ratio of prose to code is here, it is -not what the findings track. That ratio is deliberately not quoted, because quoting a measurement -this section takes no position on would be an uncited figure inside the argument against uncited -figures. - -The thing to watch is that in `windows-waitable-queues`, a handful of single facts -- `Perpetual`'s -reservation-count ceiling, `Balanced`'s recurrence horizon, `Perpetual`'s position span, `Balanced`'s -field ceiling -- are each restated many times across several files, by hand, with nothing checking -any of them. Which of them has the most copies was counted once, during the review rounds that -produced this section, and has not been counted since; no census is committed, so that ordering is -recorded here as a historical observation rather than a current fact. - -**The exact counts are deliberately not recorded here.** An earlier version of this section carried -them as a table, and the table drifted within days: one row gained an occurrence when a qualifier was -added to a rustdoc elsewhere in this same branch, so the census of restatements became a restatement -that needed maintaining. That is the section's own subject, demonstrated on the section. - -Anyone who wants current numbers can compute them, which is the point of the principle below -- the -counts are a finding, and a finding should be computed rather than quoted: - -```powershell -# Occurrences of a figure across the crate, and how many files carry it. -$files = git ls-files 'crates/windows-waitable-queues/*' | - Where-Object { $_ -match '\.(rs|md|toml)$' } -foreach ($pattern in '\b255\b', '37 seconds', '2\^56', '4,294,967,295', 'about 20 years') { - $hits = 0; $carrying = 0 - foreach ($file in $files) { - $n = ([regex]::Matches([System.IO.File]::ReadAllText($file), $pattern)).Count - if ($n) { $hits += $n; $carrying++ } - } - "{0,-16} {1,3} occurrences across {2} files" -f $pattern, $hits, $carrying -} -``` - -**All of these are restated by hand with nothing checking them.** Three of those facts -- the -ceiling, the span and the field ceiling -- follow from `ClaimLayout`'s associated constants. The -time figures follow from a field width *and* an assumed sustained push rate, so a -constants-versus-table check would validate the constant-derived facts outright and the time figures -only once the -rate is pinned somewhere single. That distinction bounds what the cheapest remedy below can do -- an -earlier version of this paragraph said every one was derivable from the constants, which overstated -it, in a note about overstatement. The error surface is proportional to how often a fact is restated, -not to total prose volume: a uniform cut to the prose leaves every restatement in place, just in -fewer words. - -### Which errors this predicts, and which it does not - -Sorting PR #90's findings across all rounds by class: - -- **Restated derivable facts** -- the `2^31`/`2^30` target-dependent capacity, `MAX_RESERVED` - conflated with capacity, "`Wide` removes it" for a bound that is finite, stale recurrence tables, - a test count that matched no crate, the same horizon left unqualified across seven sites, a - withdrawn magnitude surviving in two public rustdocs. -- **Structural** -- an unmarked supersedence row in a decision index, an orphaned milestone - reference. A linter's job, not a specification's. -- **Evidence overclaiming** -- a noise floor computed from two runs, a refusal-count argument that - did not reproduce in direction or magnitude across three re-measurements. These were the most - valuable findings of the whole PR, and *more* measurement is what fixes them, not less prose. -- **Policy** -- client prescriptions surviving [D-no-client-prescriptions](crates/windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions). - Only a reviewer catches these. -- **Algorithm properties** -- **zero findings, in any round.** - -That last line is the one to be careful with, because it has two readings and only the second is -honest. There are no findings in that class because **there is no instrument for it**, not because -the algorithms are known good. `SH-14.1` is a live, known defect in the claim protocol; it was found -by a person reasoning carefully, and nothing in the toolchain would have caught it. Absence of -findings where nothing looks is not evidence of correctness -- the same error this repository has -corrected in its own measurements more than once. - -### What follows - -Three conclusions, of which the middle one is the one that changes practice. - -**Formal specification and prose reduction address different classes.** TLA+ and `loom` -([D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31)) target algorithm properties. Neither -has been run, and neither is scheduled: `D-31` records the `loom` verification as planned, and -several documents name `M31.6` as its owner, but no checklist contains that item -- -`windows-waitable-queues` has an archive, -[COMPLETED-CHECKLIST.md](crates/windows-waitable-queues/COMPLETED-CHECKLIST.md), and no open -checklist at all. So what can be said about that class is -that it produced no findings in any review round of PR #90 -while carrying one known unfound defect, which is a statement about the reviews rather than a -result from either instrument. Restatement targets documented facts, -which have produced most findings. Both are worth doing; conflating them would aim the expensive -instrument at the cheap problem. - -**The cut must be to restated assertions, not to rationale.** No finding in any round of PR #90 was -against a passage explaining *why* a decision was made. The findings were against duplicated -*assertions* of fact, against overclaims from evidence, and against prescriptions. Rationale is what -makes a decision re-checkable years later and is the reason this file exists at all; cutting it -uniformly to hit a volume target would remove the only prose that has never been wrong, while -leaving the prose that keeps being wrong in proportion. - -**A formal spec's most useful property here is not proof -- it is that prose can point at it instead -of paraphrasing it.** That is [restatement drift](#restatement-drift)'s first remedy applied one -level up: define the protocol once in a form that can be checked, and let every document cite it. -This is the real connection between the two ideas, and it is why they belong in the same -conversation despite fixing different things. - -### Prose carries the claim; an artifact carries the number - -The sharper question, asked after several rounds of the above: **why is measured data living in -prose at all?** - -There is no principled reason. It is an accident of what is easy. Markdown has no include and -rustdoc has no data include, so the only way to put a figure in front of a reader is to paste it -- -and a pasted figure is a copy somebody must keep true by hand, in every place they pasted it, -forever. - -The cost is measurable in this PR's own review history. Almost none of its measurement-related -findings were *wrong measurements*. They were **transcription failures**: the same table in the -README and the crate rustdoc disagreeing because one was retaken; an attribution naming a capture -the figures no longer came from; one recurrence horizon left unqualified across seven sites in three -wordings; a withdrawn magnitude surviving in two public rustdocs. The most instructive was a -proportion that restated two counts **given four words earlier in the same sentence** and got one of -them wrong -- it said "in both cases roughly 60%" where one of the two cases was 57 of 61. The data -was adjacent and the summary of it was false, because prose is not checkable and nobody checks it. - -**This repository already contains the better pattern, and this branch was the first to apply it -in the probe crate.** -[`mutation-sweeps/2026-09-02/`](mutation-sweeps/2026-09-02) is a dated, committed capture directory: data as an artifact, cited -rather than retyped. `windows-platform-probes` produces the most-cited numbers in the workspace and -committed no capture at all when this section was written -- every figure it had published reached -its document by hand. The re-measurement that `M4.3` forced is the first exception: -[`crates/windows-platform-probes/captures/2026-09-16-drained-handshake/`](crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md) -commits the raw runs, the script that derives the summary, and its output. The seven-run sweep that -the variance argument rests on still has no committed capture, so the gap this section describes is -narrowed rather than closed. +**Decided: the error surface is proportional to how often a fact is restated, not to how much prose +carries it.** A uniform cut to the prose leaves every restatement in place, just in fewer words. +What is watched is therefore the number of hand-maintained copies of a single fact, not a word or +line count -- and no prose-to-code ratio is quoted here, because this note takes no position on one. -So the principle, which holds regardless of which mechanism is eventually chosen: +**Decided: prose carries the claim; an artifact carries the number.** - **A claim belongs in prose.** "`reserving_mpsc` measured faster than `slotwise_mpsc` under contention, over a spread that overlaps the same-code control at every producer count" is a @@ -2017,47 +1881,30 @@ So the principle, which holds regardless of which mechanism is eventually chosen checked by nobody, and stale the moment any input moves. The counts are the finding. A reader who wants a ratio can take one, against a denominator they chose and at a moment they know. -If this were adopted, the "which restatements are mechanically checkable" question earlier in this -note **dissolves** rather than being answered: all of them, because none would be restated. - -**The mechanism is undecided and no work is scheduled here.** The reader-experience trade is real -- -a figure in the prose is read by whoever reads the sentence, and a figure behind a link is read by -whoever follows it, which is a different and unmeasured set -- and it has not been settled. -Recorded as a principle so the next person choosing where to paste a number has the argument in front -of them, not as a queued change. Per "design notes are not a work queue", the absence of a checklist -item is deliberate. - -### The cheapest available move, recorded but not scheduled - -[README.md](crates/windows-waitable-queues/README.md) is already a build input for -`windows-waitable-queues` (`#[doc = include_str!]` in -[lib.rs](crates/windows-waitable-queues/src/lib.rs)), so a test can parse the published layout -tables and assert every row against `ClaimLayout`'s constants -- turning the occurrences that sit in -table rows into checked derivations of one definition, with no generator and no new tooling. It -reaches only those; the occurrences in prose are untouched by it. - -**Be precise about what that would and would not catch, because this paragraph has now overstated it -twice.** The layout table's columns are the layout name, the reservation-count field ceiling, the -pushes-to-recurrence count, and a time. A constants check covers the **ceiling and push-count -columns** outright. The time column additionally needs the assumed rate pinned somewhere single. And -the two errors this note originally named -- the `2^31`/`2^30` target-dependent capacity and the -`MAX_RESERVED`-as-capacity conflation -- it would **not** have caught at all: both are prose -assertions in the surrounding text, not cells in any table. - -That bound is the useful part rather than a caveat on it. A constants-versus-table check reaches the -occurrences that sit in table rows and none of the ones in prose, and both populations are -substantial -- which is the shape of the result, and a reason to build the check rather than not to. -The prose occurrences need something that reads assertions rather than rows. A remedy that covers the -tabular ones is worth having; claiming it covers both is how a partial instrument comes to be trusted -as a complete one. - -*(An earlier version of this paragraph put a proportion here. It is gone deliberately: a ratio over -the counts above is a restatement of them, computed by hand and checked by nobody, and it drifts the -moment any file is edited -- which is the defect this whole note is about. The counts are the -finding. Anyone who needs a proportion can take one, against a denominator they chose and at a moment -they know.)* - -**No work is scheduled by this note.** It was written to inform a decision that has not been taken, -and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule -rather than an oversight. If the table-versus-constants test or a -prose-reduction pass is adopted, each needs its own item at that time. +[`mutation-sweeps/2026-09-02/`](mutation-sweeps/2026-09-02) and +[`crates/windows-platform-probes/captures/2026-09-16-drained-handshake/`](crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md) +are the worked examples: dated, committed capture directories carrying raw runs, the script that +derives the summary, and its output. The seven-run sweep the variance argument rests on still has no +committed capture, so the gap is narrowed rather than closed. + +**Decided: formal specification and restatement control address different classes, and neither +substitutes for the other.** TLA+ and `loom` +([D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31)) target algorithm properties; +restatement control targets documented facts. A formal spec's most useful property here is not proof +-- it is that prose can point at it instead of paraphrasing it, which is +[restatement drift](#restatement-drift)'s first remedy applied one level up. + +Neither of those instruments has been run, and neither is scheduled: `D-31` records the `loom` +verification as planned and several documents name `M31.6` as its owner, but no checklist contains +that item -- `windows-waitable-queues` has an archive, +[COMPLETED-CHECKLIST.md](crates/windows-waitable-queues/COMPLETED-CHECKLIST.md), and no open +checklist at all. + +**Decided: the cut is to restated assertions, never to rationale.** Rationale is what makes a +decision re-checkable years later; cutting it to hit a volume target would remove the only prose +that has never been wrong while leaving the prose that keeps being wrong in proportion. + +**No work is scheduled by this note**, per "design notes are not a work queue". How the measurement +was taken, which findings it is drawn from, what was rejected on the way, and the one cheap remedy +that was costed but not adopted are in +[DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#why-restatement-count-is-what-is-watched). diff --git a/DESIGN-RATIONALE.md b/DESIGN-RATIONALE.md index 08eb9641e..cce9fa325 100644 --- a/DESIGN-RATIONALE.md +++ b/DESIGN-RATIONALE.md @@ -207,8 +207,8 @@ each produce a distinct, located failure. ## Why a measured figure is asked to have one home -[DESIGN-NOTES.md](DESIGN-NOTES.md)'s restatement-drift section records the rule; this is how it was -reached, and what was rejected on the way. +[DESIGN-NOTES.md](DESIGN-NOTES.md#prose-volume-and-error-surface) records the rule; this is how it +was reached, and what was rejected on the way. The evidence was a review history, not an argument. Across the rounds on PR #90, most findings were not wrong measurements -- they were transcriptions that had drifted from the thing they restated: a @@ -233,6 +233,214 @@ The mechanism -- how a figure gets from an artifact into rendered prose -- is de open; markdown has no include, and rustdoc's include is whole-file. That is stated in the decision as an unsettled trade rather than resolved here, and no work is scheduled against it. +## Why restatement count is what is watched + +[DESIGN-NOTES.md](DESIGN-NOTES.md#prose-volume-and-error-surface) records the decision. This is how +it was reached, what was rejected on the way, and the remedy that was costed but not adopted. It was +moved here from that file, where it had been written inline: Tier 1 is the current decision, and a +section carrying its own motivating question, census procedure and superseded drafts had made the +decision harder to find inside it. + +[Restatement drift](#restatement-drift) explains the mechanism and gives the remedy. This note +records something that section does not: a measurement of **where** the drift actually lives, taken +after PR #90's eighteenth review round, and what follows from it about formal specification. + +The question that prompted it was whether this repository simply says too much -- whether English, +which must be inexact to serve human readers, is being asked to carry a specification load it cannot +bear, and whether some formal specification plus substantially less prose would shrink the error +surface. + +### The measurement, and why it is not written down here + +Prose volume was looked at first and set aside: whatever the ratio of prose to code is here, it is +not what the findings track. That ratio is deliberately not quoted, because quoting a measurement +this section takes no position on would be an uncited figure inside the argument against uncited +figures. + +The thing to watch is that in `windows-waitable-queues`, a handful of single facts -- `Perpetual`'s +reservation-count ceiling, `Balanced`'s recurrence horizon, `Perpetual`'s position span, `Balanced`'s +field ceiling -- are each restated many times across several files, by hand, with nothing checking +any of them. Which of them has the most copies was counted once, during the review rounds that +produced this section, and has not been counted since; no census is committed, so that ordering is +recorded here as a historical observation rather than a current fact. + +**The exact counts are deliberately not recorded here.** An earlier version of this section carried +them as a table, and the table drifted within days: one row gained an occurrence when a qualifier was +added to a rustdoc elsewhere in this same branch, so the census of restatements became a restatement +that needed maintaining. That is the section's own subject, demonstrated on the section. + +Anyone who wants current numbers can compute them, which is the point of the principle below -- the +counts are a finding, and a finding should be computed rather than quoted: + +```powershell +# Occurrences of a figure across the crate, and how many files carry it. +$files = git ls-files 'crates/windows-waitable-queues/*' | + Where-Object { $_ -match '\.(rs|md|toml)$' } +foreach ($pattern in '\b255\b', '37 seconds', '2\^56', '4,294,967,295', 'about 20 years') { + $hits = 0; $carrying = 0 + foreach ($file in $files) { + $n = ([regex]::Matches([System.IO.File]::ReadAllText($file), $pattern)).Count + if ($n) { $hits += $n; $carrying++ } + } + "{0,-16} {1,3} occurrences across {2} files" -f $pattern, $hits, $carrying +} +``` + +**All of these are restated by hand with nothing checking them.** Three of those facts -- the +ceiling, the span and the field ceiling -- follow from `ClaimLayout`'s associated constants. The +time figures follow from a field width *and* an assumed sustained push rate, so a +constants-versus-table check would validate the constant-derived facts outright and the time figures +only once the +rate is pinned somewhere single. That distinction bounds what the cheapest remedy below can do -- an +earlier version of this paragraph said every one was derivable from the constants, which overstated +it, in a note about overstatement. The error surface is proportional to how often a fact is restated, +not to total prose volume: a uniform cut to the prose leaves every restatement in place, just in +fewer words. + +### Which errors this predicts, and which it does not + +Sorting PR #90's findings across all rounds by class: + +- **Restated derivable facts** -- the `2^31`/`2^30` target-dependent capacity, `MAX_RESERVED` + conflated with capacity, "`Wide` removes it" for a bound that is finite, stale recurrence tables, + a test count that matched no crate, the same horizon left unqualified across seven sites, a + withdrawn magnitude surviving in two public rustdocs. +- **Structural** -- an unmarked supersedence row in a decision index, an orphaned milestone + reference. A linter's job, not a specification's. +- **Evidence overclaiming** -- a noise floor computed from two runs, a refusal-count argument that + did not reproduce in direction or magnitude across three re-measurements. These were the most + valuable findings of the whole PR, and *more* measurement is what fixes them, not less prose. +- **Policy** -- client prescriptions surviving [D-no-client-prescriptions](crates/windows-platform-probes/DESIGN-NOTES.md#d-no-client-prescriptions). + Only a reviewer catches these. +- **Algorithm properties** -- **zero findings, in any round.** + +That last line is the one to be careful with, because it has two readings and only the second is +honest. There are no findings in that class because **there is no instrument for it**, not because +the algorithms are known good. `SH-14.1` is a live, known defect in the claim protocol; it was found +by a person reasoning carefully, and nothing in the toolchain would have caught it. Absence of +findings where nothing looks is not evidence of correctness -- the same error this repository has +corrected in its own measurements more than once. + +### What follows + +Three conclusions, of which the middle one is the one that changes practice. + +**Formal specification and prose reduction address different classes.** TLA+ and `loom` +([D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31)) target algorithm properties. Neither +has been run, and neither is scheduled: `D-31` records the `loom` verification as planned, and +several documents name `M31.6` as its owner, but no checklist contains that item -- +`windows-waitable-queues` has an archive, +[COMPLETED-CHECKLIST.md](crates/windows-waitable-queues/COMPLETED-CHECKLIST.md), and no open +checklist at all. So what can be said about that class is +that it produced no findings in any review round of PR #90 +while carrying one known unfound defect, which is a statement about the reviews rather than a +result from either instrument. Restatement targets documented facts, +which have produced most findings. Both are worth doing; conflating them would aim the expensive +instrument at the cheap problem. + +**The cut must be to restated assertions, not to rationale.** No finding in any round of PR #90 was +against a passage explaining *why* a decision was made. The findings were against duplicated +*assertions* of fact, against overclaims from evidence, and against prescriptions. Rationale is what +makes a decision re-checkable years later and is the reason this file exists at all; cutting it +uniformly to hit a volume target would remove the only prose that has never been wrong, while +leaving the prose that keeps being wrong in proportion. + +**A formal spec's most useful property here is not proof -- it is that prose can point at it instead +of paraphrasing it.** That is [restatement drift](#restatement-drift)'s first remedy applied one +level up: define the protocol once in a form that can be checked, and let every document cite it. +This is the real connection between the two ideas, and it is why they belong in the same +conversation despite fixing different things. + +### Prose carries the claim; an artifact carries the number + +The sharper question, asked after several rounds of the above: **why is measured data living in +prose at all?** + +There is no principled reason. It is an accident of what is easy. Markdown has no include and +rustdoc has no data include, so the only way to put a figure in front of a reader is to paste it -- +and a pasted figure is a copy somebody must keep true by hand, in every place they pasted it, +forever. + +The cost is measurable in this PR's own review history. Almost none of its measurement-related +findings were *wrong measurements*. They were **transcription failures**: the same table in the +README and the crate rustdoc disagreeing because one was retaken; an attribution naming a capture +the figures no longer came from; one recurrence horizon left unqualified across seven sites in three +wordings; a withdrawn magnitude surviving in two public rustdocs. The most instructive was a +proportion that restated two counts **given four words earlier in the same sentence** and got one of +them wrong -- it said "in both cases roughly 60%" where one of the two cases was 57 of 61. The data +was adjacent and the summary of it was false, because prose is not checkable and nobody checks it. + +**This repository already contains the better pattern, and this branch was the first to apply it +in the probe crate.** +[`mutation-sweeps/2026-09-02/`](mutation-sweeps/2026-09-02) is a dated, committed capture directory: data as an artifact, cited +rather than retyped. `windows-platform-probes` produces the most-cited numbers in the workspace and +committed no capture at all when this section was written -- every figure it had published reached +its document by hand. The re-measurement that `M4.3` forced is the first exception: +[`crates/windows-platform-probes/captures/2026-09-16-drained-handshake/`](crates/windows-platform-probes/captures/2026-09-16-drained-handshake/README.md) +commits the raw runs, the script that derives the summary, and its output. The seven-run sweep that +the variance argument rests on still has no committed capture, so the gap this section describes is +narrowed rather than closed. + +So the principle, which holds regardless of which mechanism is eventually chosen: + +- **A claim belongs in prose.** "`reserving_mpsc` measured faster than `slotwise_mpsc` under + contention, over a spread that overlaps the same-code control at every producer count" is a + claim. It transcribes no figure, so it cannot drift from the artifact the way a pasted number + does -- but it is not thereby permanent: a retake can make it false, and a reader cannot tell from + the sentence alone. That is why the claim cites the artifact. Dropping the digits removes the + transcription failure and leaves the citation obligation exactly where it was. +- **A number belongs in an artifact.** A measured cost, a capture's commit, a count of occurrences: + one copy, with its provenance travelling *with* it rather than in a hand-maintained attribution + table beside it. +- **A proportion over data we hold is not a finding, it is a restatement of one.** Computed by hand, + checked by nobody, and stale the moment any input moves. The counts are the finding. A reader who + wants a ratio can take one, against a denominator they chose and at a moment they know. + +If this were adopted, the "which restatements are mechanically checkable" question earlier in this +note **dissolves** rather than being answered: all of them, because none would be restated. + +**The mechanism is undecided and no work is scheduled here.** The reader-experience trade is real -- +a figure in the prose is read by whoever reads the sentence, and a figure behind a link is read by +whoever follows it, which is a different and unmeasured set -- and it has not been settled. +Recorded as a principle so the next person choosing where to paste a number has the argument in front +of them, not as a queued change. Per "design notes are not a work queue", the absence of a checklist +item is deliberate. + +### The cheapest available move, recorded but not scheduled + +[README.md](crates/windows-waitable-queues/README.md) is already a build input for +`windows-waitable-queues` (`#[doc = include_str!]` in +[lib.rs](crates/windows-waitable-queues/src/lib.rs)), so a test can parse the published layout +tables and assert every row against `ClaimLayout`'s constants -- turning the occurrences that sit in +table rows into checked derivations of one definition, with no generator and no new tooling. It +reaches only those; the occurrences in prose are untouched by it. + +**Be precise about what that would and would not catch, because this paragraph has now overstated it +twice.** The layout table's columns are the layout name, the reservation-count field ceiling, the +pushes-to-recurrence count, and a time. A constants check covers the **ceiling and push-count +columns** outright. The time column additionally needs the assumed rate pinned somewhere single. And +the two errors this note originally named -- the `2^31`/`2^30` target-dependent capacity and the +`MAX_RESERVED`-as-capacity conflation -- it would **not** have caught at all: both are prose +assertions in the surrounding text, not cells in any table. + +That bound is the useful part rather than a caveat on it. A constants-versus-table check reaches the +occurrences that sit in table rows and none of the ones in prose, and both populations are +substantial -- which is the shape of the result, and a reason to build the check rather than not to. +The prose occurrences need something that reads assertions rather than rows. A remedy that covers the +tabular ones is worth having; claiming it covers both is how a partial instrument comes to be trusted +as a complete one. + +*(An earlier version of this paragraph put a proportion here. It is gone deliberately: a ratio over +the counts above is a restatement of them, computed by hand and checked by nobody, and it drifts the +moment any file is edited -- which is the defect this whole note is about. The counts are the +finding. Anyone who needs a proportion can take one, against a denominator they chose and at a moment +they know.)* + +**No work is scheduled by this note.** It was written to inform a decision that has not been taken, +and the deliberate absence of a checklist item is per the "design notes are not a work queue" rule +rather than an oversight. If the table-versus-constants test or a +prose-reduction pass is adopted, each needs its own item at that time. + ## References - [`QueueUserWorkItem` and `WT_TRANSFER_IMPERSONATION`](https://learn.microsoft.com/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-queueuserworkitem) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index c86b6107b..bca71d524 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -237,22 +237,6 @@ correctness in the archive. them. Until this lands, a shape that needs two dimensions must be added by hand, which is exactly the imagination-driven process M2.12 exists to replace. -- [ ] **M4.9** -- Move the prose-volume section's history out of Tier 1. - - **Gap:** `DESIGN-NOTES.md`'s "Prose volume is not the error surface" section carries its motivating - question, the alternatives rejected, the review-history it was measured from, an earlier draft it - corrects, and an undecided mechanism. Tier 1 is for the current decision; Tier 2 - ([DESIGN-RATIONALE.md](../../DESIGN-RATIONALE.md)) already holds the history for the neighbouring - one-home rule, so this section splits its own rationale across both tiers and makes the decision - harder to find inside it. - - **Target:** a compact decision in `DESIGN-NOTES.md` -- what is watched, and why restatement count - rather than volume -- with the narrative moved to `DESIGN-RATIONALE.md` beside the rule it belongs - with, per the three-tier convention. No content is dropped; it changes which file owns it. - - **Blocker recorded when queued:** none. Raised by review during PR #90 and accepted; deferred only - because a move of this size is safer as its own commit than appended to a review round. - - [ ] **M4.8** -- Have the queue-contention report carry the build identity that produced it, and have the capture scripts require it to agree. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index b603d0b7e..06a962a08 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1189,7 +1189,7 @@ consumer has reached its first `pop`, and it releases every party together. So a into a queue nobody was draining yet -- an undrained opening to a run whose whole subject is that it is drained. `await_consumer` closes it: the consumer announces that it is draining, and producers hold until they see that before starting their clocks. Applied to all four drained timers, with -`Acquire`/`Release` per the queue crate's `D-40` standing answer on promoting the load. +`Acquire`/`Release` per the queue crate's `D-38` standing answer on promoting the load. **The blocker recorded when this was queued was real, and it is what made the item large.** The change moves the drained numbers, so every drained figure already published measured a different diff --git a/crates/windows-platform-probes/Cargo.toml b/crates/windows-platform-probes/Cargo.toml index cef8b8d87..1b3ff7259 100644 --- a/crates/windows-platform-probes/Cargo.toml +++ b/crates/windows-platform-probes/Cargo.toml @@ -182,7 +182,7 @@ serde = { version = "1.0", optional = true } # This is not a tuning choice, it is the queue crate's `D-18`/`D-37` contract: # `dwcas` is non-default precisely so `reserving_mpsc`'s narrow word ships on # every target while the wide one ships only where it is genuinely lock-free. -# `i686-pc-windows-msvc` has no 128-bit atomic at all, and `D-40` records that +# `i686-pc-windows-msvc` has no 128-bit atomic at all, and `D-18` records that # the workspace deliberately keeps that target supported -- so enabling `dwcas` # from here without a target gate would propagate through feature unification and # fail the i686 build in the queue crate (`cannot find AtomicU128 in diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs index 69112b3af..d12392e1d 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -2,7 +2,7 @@ //! The report renderer, driven by a corpus rather than by hand-written cases. //! -//! Adding a case is adding data to `corpus.json` -- an observation, and what the +//! Adding a case is adding data to [corpus.json](corpus.json) -- an observation, and what the //! rendered report must be true of. Nothing here needs to change, which is the //! point: the report's tables were reachable by nothing in the suite until //! `M4.7`, because rendering measured inside itself and the only way to run it diff --git a/crates/windows-platform-probes/src/queue_contention.rs b/crates/windows-platform-probes/src/queue_contention.rs index 13e30d800..9fe291a3c 100644 --- a/crates/windows-platform-probes/src/queue_contention.rs +++ b/crates/windows-platform-probes/src/queue_contention.rs @@ -1209,7 +1209,7 @@ impl Drop for StopOnDrop { /// /// `Acquire`/`Release` rather than `Relaxed`, though the flag carries no data: /// this is the standing "promote the load" answer recorded in the queue crate's -/// [D-40](../../windows-waitable-queues/DESIGN-NOTES.md#d-40) -- an acquire that +/// [D-38](../../windows-waitable-queues/DESIGN-NOTES.md#d-38) -- an acquire that /// proves unnecessary costs little, while a relaxed load that turns out to have /// been load-bearing fails only on hardware nobody here owns. /// From 5dc1058efaf4c992731ee446f178ae27a5199b69 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 01:49:11 -0400 Subject: [PATCH 138/139] fix(probes): read corpus counts as integers, and reject a zero processor count Two ways the corpus parser accepted a value the probe could never produce. `whole` read counts through `as_f64` and cast back, so a value past 2^53 rounded silently -- 9007199254740993 becomes ...992 -- while every validation passed. Read as `u64` directly, which is what the field is. `available_parallelism` comes from `NonZeroUsize`, so zero is not a count the probe can observe; `null` is the only representation of a failed query. The match accepted `0` and rendered it as an ordinary processor count. Reported by review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/queue_contention/tests.rs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs index d12392e1d..c233eba1e 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -61,16 +61,15 @@ fn run_from(value: &Value) -> Run { .as_f64() .unwrap_or_else(|| panic!("field {index} of a run is a number")) }; - // `as` truncates a fraction and saturates a negative, so `1.5` would become - // producer count 1 and `-3` would become 0 -- a fixture exercising a case it - // does not name, silently. Counts are checked for being counts first. + // Read as an integer, not through `f64`. `as_f64` rounds past 2^53, so a + // refusal count of 9007199254740993 would silently become ...992; and `as` + // on the way back truncates a fraction and saturates a negative, so `1.5` + // would become producer count 1. A fixture must fail rather than quietly + // exercise a case it does not name. let whole = |index: usize| -> u64 { - let value = number(index); - assert!( - value.is_finite() && value >= 0.0 && value.fract() == 0.0, - "field {index} of a run is a non-negative whole number, not {value}" - ); - value as u64 + row[index] + .as_u64() + .unwrap_or_else(|| panic!("field {index} of a run is a non-negative whole number")) }; Run { // Leaked so the fixture can hand back the `&'static str` the field @@ -118,18 +117,19 @@ fn observation_from(value: &Value) -> Observation { panic!("a case states `available_parallelism`, using null where the query failed") }) { Value::Null => None, - other => Some( - other - .as_u64() - .unwrap_or_else(|| { - panic!( - "`available_parallelism` is a non-negative whole number or null, \ - not {other}" - ) - }) - .try_into() - .expect("a processor count fits a usize"), - ), + other => { + // The real value comes from `NonZeroUsize`, so zero is not a + // processor count the probe can ever observe. `null` is the only + // representation of a failed query; accepting `0` as well would + // let a malformed corpus publish an impossible figure. + let count = other.as_u64().unwrap_or_else(|| { + panic!( + "`available_parallelism` is a positive whole number or null, not {other}" + ) + }); + assert!(count > 0, "`available_parallelism` is null when the query failed, not 0"); + Some(usize::try_from(count).expect("a processor count fits a usize")) + } }, } } From 3d8aba72e4b43c4faada734c261f60e80cf5eba0 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 17 Sep 2026 01:52:04 -0400 Subject: [PATCH 139/139] style(probes): cargo fmt the corpus parser's zero-count assertion --- .../src/bin/queue_contention/tests.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs index c233eba1e..6eb6992d3 100644 --- a/crates/windows-platform-probes/src/bin/queue_contention/tests.rs +++ b/crates/windows-platform-probes/src/bin/queue_contention/tests.rs @@ -127,7 +127,10 @@ fn observation_from(value: &Value) -> Observation { "`available_parallelism` is a positive whole number or null, not {other}" ) }); - assert!(count > 0, "`available_parallelism` is null when the query failed, not 0"); + assert!( + count > 0, + "`available_parallelism` is null when the query failed, not 0" + ); Some(usize::try_from(count).expect("a processor count fits a usize")) } },