Measure the handshake and hold bounds under deliberate contention, so they can be sized on wall clock rather than counts
- Ngôn ngữ chính
- Rust
- Star
- 9
- Fork
- 1
- Merge trung bình
- 11 giờ 46 phút
- Pull request đã merge (30 ngày)
- 62
Mô tả
## Background
#305 (PR #341) lowered `HANDSHAKE_YIELDS` from 1,000,000 to 250,000 and measured the yields four conformance rows actually spend reaching their commit, across all six CI legs and 20 local suite runs. Worst observed: 155.
That table sizes the bound in *counts*. It does not size it in wall clock, which is what a bound has to survive, and #341 could not close that gap from the machines available to it.
## Why the counts do not settle it
**The sampled waits are in a different regime from the one the margin protects.** Every count above was spent on a wait that *ended at a commit*. The margin, though, exists for the run that is correct but merely late — and for the failing run whose reporting time the value also sets. Those are not the same condition, and the per-yield rate differs across them by three orders of magnitude:
| condition | per yield |
| --- | --- |
| release build, idle | ~0.15 µs |
| release build, 8 competing spinners | ~2.1 µs |
| debug build, blocked peer | ~154 µs |
The failing path's rate is measured, not extrapolated: #305 records a row that spent the whole 1,000,000 taking **154.46 s** to report, which is what makes 250,000 worth ~38 s. What is *not* measured for this loop is the runnable-but-unscheduled regime, which is where a correct-but-late commit sits, and where 250,000 is somewhere between ~37 ms and ~525 ms depending on which borrowed rate applies.
**An idle host cannot measure the failing path.** Driving the unanswerable wait three times at each of three bounds on an idle 10-core machine:
| bound | seconds to report |
| --- | --- |
| 50,000 | 0.30, 0.33, 0.09 |
| 250,000 | 0.07, 0.07, 0.07 |
| 1,000,000 | 0.85, 1.47, 1.44 |
Non-monotone — 250,000 measured *faster* than 50,000 — because the loop's wall clock there is scheduling noise rather than a yield rate. No per-yield figure can be read off it.
**The tail is unsampled, and the factor for it is not pinned down.** 155 comes from one Windows leg; that leg's second run peaked at 34. The tree's nearest precedent is `HOLD_YIELDS`'s macOS outlier of 6664 — taken under *incidental* contention, a run that happened to have a build beside it, not the deliberately oversubscribed sampling this issue asks for. How large a tail it represents depends on what it is compared against: 18x its column's maximum of 373, or up to 98x its minimum of 68, while its own doc calls it "a thirtyfold tail" without recording the denominator.
`HANDSHAKE_YIELDS`'s doc takes the 30x, as the conservative choice: the real worst lands near 4,650 and the headroom at roughly 54x rather than 1,600x. Pinning that factor down is part of what a contended measurement buys — the 54x is currently the conservative end of a range this repository cannot close from what it has recorded.
## What would close it
A contended measurement of these loops on a host where the expensive rate is reproducible — the shape `HOLD_YIELDS`'s own sampling used, and which its doc credits for the decisive numbers: 100 Linux runs at 4x oversubscription, plus a macOS sample taken with a build running beside it.
Specifically:
- The per-yield cost of `open_and_await_commit`'s loop under contention, rather than borrowing `HOLD_YIELDS`'s figure for the regimes where this loop has none.
- The yields a *passing* commit spends on an oversubscribed runner — the row neither constant's table has, and the one that decides how tight the bound can be.
- The same for `HOLD_YIELDS`, whose 262,144 reports in ~40 s on the borrowed rate and has never been checked against a rate measured for its own wait. Its doc says "around 45 seconds"; the multiplication gives 40.4 s, and the tree does not record which of the two was measured.
## A design option that would remove the trade rather than tune it
The bound is one number serving two purposes that pull opposite ways: how fast a real failure reports, and how long a correct-but-late producer may take before the row panics. Lowering it improves the first and worsens the second in exact proportion.
Backing off after the first N yields — parking briefly rather than spinning — decouples them: a late producer gets far more wall clock per iteration while an unanswered handshake still ends promptly. It conflicts with `open_and_await_commit`'s stated contract that the wait "ends either at the commit or at its bound, and never on a clock", so it needs that contract revisited rather than a value edit.
## Why deferred
#341's scope was the value and its rationale. A contended sampling campaign needs a host deliberately loaded for it, which is a different kind of work from that PR, and running one is a decision about shared compute rather than a code change.
## What #341 settled without this measurement, and what it did not
#341 chose 250,000 as a **decision rule** rather than as a measured value: the Windows Server quantum is ~187 ms, `SwitchToThread` does not reach a producer queued on another core, and 50,000 spends its whole budget inside one quantum at either borrowed rate while 250,000 crosses it at the contended end. Since the tail is unmeasured either way, it picked the side where being wrong is cheaper — a slow report paid once, rather than a required check that flakes for everyone.
What this issue would replace: the rule, with a number. Both directions are open. A contended measurement showing the passing path never approaches the bound would justify going back down toward the single-digit-second reporting #305 asked for; one showing it can approach a quantum would justify staying here or going higher.
Hướng dẫn đóng góp
Đánh giá
Issue này chưa được đánh giá.