Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Noise gates

Detection produces history candidates whose chance levels already account for the detector’s within-series choices, and branch candidates that lie outside an observed current-base range. This chapter is the set of checks that decides which candidates are worth your attention.

Every gate answers the same underlying question in a different way: is this move larger than what could be explained by measurement process noise? A benchmark that reports 100 ns will typically not report exactly 100 ns the next time. The gates exist to keep that fact from becoming a stream of false alarms — and every one of them is a filter: a gate can only ever remove a candidate, never create one or strengthen another.

Terms used here

TermWhat it means
agreement shareThe fraction of before-and-after pairs that agree the level moved in the same direction.
confidence intervalA range the benchmark engine reports alongside a measurement to say how precisely it pinned it down.
typical residualHow far a representative point sits from the history model fitted to the series: a level for a change point, a line for a drift.

What the gates are, and are not

The gates weigh evidence, never cause.

This is worth stating plainly because it is the most common misreading. If a compiler upgrade makes your benchmark 8% slower, that is a real move and the tool will report it. The gates will not suppress it for being “not your fault” — they cannot know whose fault it is, and a tool that guessed would be worse than one that does not try.

Deciding a reported move is acceptable is your call, and bless is how you record it. What the gates remove is movement that is not there: jitter, sampling luck, and moves too small to act on.

The questions the gates ask

There are more gates than there are questions, because several detectors ask the same question at a different point in their own sequence. The questions are the part worth learning:

QuestionGates that ask it
Is there enough data to judge a candidate at all?min_series_points, min_regime, min_base_commits
Did anything actually change?split_located, non_zero_delta
Could chance alone have produced the history candidate?significance
Is the move big enough to matter?relative_floor, absolute_floor
Is a history move bigger than what this series does anyway?residual_noise
Do the two before-and-after sides genuinely separate?regime_separation
Does the engine’s own precision explain the move?interval_disjoint, interval_noise_band

The diagnostic ladder starts with Detection’s own setup and test rows so it can explain the whole verdict in one place. selection_adjustment is one of those rows: it is the within-series correction described in Detection, surfaced here for visibility. It changes the chance level and never declines a candidate by itself.

Each detector has its own sequence

There is no single gate order. Each detector applies the gates its own question needs, in the order that makes sense for it. A change point checks inexpensive magnitude and noise evidence before running its bounded search correction; a branch comparison first establishes that the context value is outside the observed range, then checks the excess beyond the nearest edge.

A candidate stops at the first gate that declines it. Nothing after that runs, which is why a gate ladder is often short. That is an accurate picture, not a truncated one.

Each detector applies its own gates in its own order, and a candidate stops at the first gate that declines it. A gate several detectors share is one policy asked at a different point in each sequence.

change_point — a level that moved and stayed moved

GateWhat it comparesThreshold
split_locatedWhether a candidate split exists at all.a split must be found
min_regimeHow many points the shorter side of the split holds.5 points
non_zero_deltaWhether the two regime levels differ at all.above zero
relative_floorThe move as a fraction of the baseline.3.0%
absolute_floorThe move in the metric’s own units.the metric’s own floor, below
residual_noiseThe move against the series’ own typical residual.3× the typical residual
regime_separationThe share of before-and-after pairs that agree the level moved.0.85
interval_disjointThe two regimes’ reported confidence intervals.the two intervals must not overlap
selection_adjustmentThe change-point’s rank-test chance level, before the split-search correction.corrects the level; never declines
significanceThe chance level of the rank test comparing the two regimes.p < 0.05

drift — a level that is moving steadily

GateWhat it comparesThreshold
min_series_pointsHow many points the analyzed window holds.10 points
significanceThe chance level of the trend test across the window.p < 0.05
non_zero_deltaWhether the fitted line moved across the window.above zero
relative_floorThe move as a fraction of the baseline.3.0%
absolute_floorThe move in the metric’s own units.the metric’s own floor, below
residual_noiseThe move against the series’ own typical residual.3× the typical residual
interval_noise_bandThe move against the engine’s own reported imprecision.2× the reported half-width

branch — a context commit against the base ref

GateWhat it comparesThreshold
min_base_commitsHow many base-ref commit levels the comparison window holds.10 commit levels
non_zero_deltaWhether the context run lies strictly outside the observed current-base range.above zero
relative_floorThe move as a fraction of the baseline.5.0%
absolute_floorThe move in the metric’s own units.the metric’s own floor, below
interval_disjointThe current-base observations’ and context run’s reported confidence intervals.the two intervals must not overlap
interval_noise_bandThe move against the engine’s own reported imprecision.2× the reported half-width

Two things in those tables are easy to miss:

  • Branch mode uses a higher relative floor than history mode. A branch comparison rests on a single context observation, and a pull request that cries wolf gets ignored, so the bar is raised.
  • The interval checks are not applied uniformly. Disjointness is checked wherever two regimes are compared — a change point, or a context commit against its base. The noise band belongs to drift and branch. A detector applies an interval check only where the comparison it makes has a meaningful interval to check.

Gate logic: is the move big enough to matter? (relative_floor, absolute_floor)

Two floors, and a candidate must clear both.

Relative — a fraction of the baseline. This is the one people expect.

Absolute — a floor in the metric’s own units, which exists because a percentage means nothing at small magnitudes. A benchmark running at 3 ns per iteration that moves to 3.2 ns has moved by more than 6% — comfortably past the relative floor — and told you nothing you can act on.

the same 6.7% move at two magnitudes at 3 ns: the move fits inside the floor ns commit position 2.0 2.5 3.0 3.5 4.0 0 2 4 6 8 10 12 14 16 18 moves smaller than 1 ns the new level at 3000 ns: reported ns commit position 3000.0 3050.0 3100.0 3150.0 3200.0 0 2 4 6 8 10 12 14 16 18 moves smaller than 1 ns the new level

The floors differ per metric because the reasons differ:

MetricAbsolute floorWhy
wall_time1 nsA timing engine fits a slope across a run’s iterations and resolves far below a clock tick, so this is a judgement about what is worth acting on rather than a resolution limit.
processor_time1 nsA timing engine fits a slope across a run’s iterations and resolves far below a clock tick, so this is a judgement about what is worth acting on rather than a resolution limit.
instruction_count5 instructionsCode layout can shift these counts by a few units between builds of identical source, so a handful of them says nothing about what the code costs.
conditional_branches5 conditional branchesCode layout can shift these counts by a few units between builds of identical source, so a handful of them says nothing about what the code costs.
indirect_branches5 indirect branchesCode layout can shift these counts by a few units between builds of identical source, so a handful of them says nothing about what the code costs.
allocated_bytes1 byteA fraction of a byte or of an allocation cannot happen; the floor rejects only the sub-unit moves that amortizing across a run’s iterations manufactures.
peak_outstanding_bytes1 byteA peak counts whole bytes; the floor rejects sub-byte differences introduced by averaging span peaks.
allocation_count1 allocationA fraction of a byte or of an allocation cannot happen; the floor rejects only the sub-unit moves that amortizing across a run’s iterations manufactures.

This pair is also what makes the tool behave sensibly across scales: the same proportional move on a benchmark a thousand times larger clears the absolute floor easily, and is reported.

Gate logic: is it bigger than what this series does anyway? (residual_noise)

This is the primary noise check, and the one that does the most work. It needs nothing from the engine — no confidence interval, no repeat runs — because it measures the series against itself.

For history, the tool fits the candidate’s own model to the series (a step for a change point, a line for a drift), then measures how far a typical point sits from that model. The move must be several times that distance. Branch mode uses its stricter observed-range rule instead.

a step against the series' typical residual and the gate threshold it must clear signed residual from the fitted model commit position -20.0 -15.0 -10.0 -5.0 0.0 5.0 10.0 15.0 20.0 0.0 2.0 4.0 6.0 8.0 10.0 12.0 14.0 16.0 18.0 residual-noise gate threshold the move being judged

The shaded band is what the series does anyway. A move inside it is indistinguishable from the series being itself.

A note on the multiplier. The typical residual is a plain median distance, not a figure rescaled to behave like a standard deviation. So “three times the typical residual” is not “three standard deviations” — for well-behaved data it is considerably stricter. Do not translate the multiple into a familiar sigma figure and reason from that.

This gate is also why a noisy benchmark is worth fixing rather than tolerating: it widens the band, and the band is what real moves have to clear. See Insights.

Gate logic: do the two sides genuinely separate? (regime_separation)

A candidate can pass a significance test and still be an artifact. The classic case is a series that oscillates between two levels: split it anywhere and the two sides differ, and with enough points a significance test will happily call that difference real.

So the tool asks a second, different question: of every possible before-and-after pairing, what share agree the level moved? That pairwise directional frequency is the agreement share — a direction-aware effect size, not another significance test.

a step: 1.00 of pairings agree, against a floor of 0.85 each observation from after the change each observation from before the change later greater tie (half) later less

A genuine step: nearly every pairing agrees.

a series that revisits both levels: 0.80 of pairings agree, against a floor of 0.85 each observation from after the change each observation from before the change later greater tie (half) later less

A series that revisits both levels leaves a substantial share of pairings contradicting the split, and the candidate is declined — which matches what a human reads off the chart, namely “noisy, unchanged”.

The important property is that this gate does not weaken as the series grows. A significance test gets easier to pass with more data, because more data makes a smaller difference detectable. An agreement share does not: it measures how completely the two sides separate, which more data estimates more precisely rather than inflating.

Gate logic: does the engine’s own precision explain the move? (interval_disjoint, interval_noise_band)

Where the engine reports a confidence interval, further checks apply. Both can only ever take a candidate away — no interval ever creates one or relaxes another gate.

Disjointness. If the two regimes’ intervals overlap, the engine is telling you it cannot distinguish these levels.

two levels the engine cannot separate ns commit position 70.0 80.0 90.0 100.0 110.0 120.0 130.0 140.0 150.0 160.0 0 2 4 6 8 10 12 14 16 18 the earlier level's interval the later level's interval change point declined by interval_disjoint

Noise band. The move must exceed a multiple of the engine’s own reported imprecision, independently of how the two levels compare.

Engines that report no interval — Callgrind always, and alloc_tracker or all_the_time for a single-span measurement — simply skip these. History still has its residual gate; branch mode still requires an excursion beyond every value actually observed in the current regime.

Two gates that fail open

Worth knowing, because it is surprising: if the tool cannot compute a gate’s input, the candidate passes rather than being declined.

A sample of a single point contributes no residual, so the residual check has nothing to compare against. A sample with no dispersion produces no interval statistic. In both cases the move is trusted.

The reasoning is that the alternative — declining anything we cannot check — would silently suppress findings on exactly the sparse series a user is least likely to be watching. But it does mean a candidate can reach the report with fewer gates actually applied than its ladder suggests.

Reading a gate ladder

Each detector’s gates form a ladder: the candidate meets them in order and stops at the first that declines it. The tables below show one row per gate, in the order the detector applies them. The demand is what the gate required, the computed value is what the candidate supplied, and the verdict states the outcome. A numeric gate passes when the computed value clears the demand. A boolean gate must simply hold. Once a gate declines, the rows below it show not run.

History compares the step and drift models after their cheap gates. If drift fits better, the step ladder can end with selection adjustment and significance marked not run even though no earlier step gate declined: the detector chose the better-fitting drift before paying for step calibration.

A candidate that clears every gate:

GateDemandComputedVerdict
split_locatedmust holdheldpass
min_regime5 points10 pointspass
non_zero_deltaabove zero29.68 nspass
relative_floor3.0%29.6%pass
absolute_floor1 ns29.68 nspass
residual_noise7.73 ns29.68 nspass
regime_separation0.851.00pass
interval_disjointmust holdheldpass
selection_adjustmentcorrected for the searchp = 1.1e-5 → p = 0.0012pass
significancep < 0.05p = 0.0024pass

And one that is declined:

Declined by residual_noise. The detector computed 15.97 ns, against a demand of 17.38 ns. The gates below it never ran.

GateDemandComputedVerdict
split_locatedmust holdheldpass
min_regime5 points10 pointspass
non_zero_deltaabove zero15.97 nspass
relative_floor3.0%15.7%pass
absolute_floor1 ns15.97 nspass
residual_noise17.38 ns15.97 nsdeclined
regime_separation0.85not runnot run
interval_disjointthe two intervals must not overlapnot runnot run
selection_adjustmentcorrects the level; never declinesnot runnot run
significancep < 0.05not runnot run

Reading a declined ladder is the fastest way to answer “why was my regression not reported?” — and Insights turns it into a checklist.

What the gates hand on

The candidates that survived. History values carry selection-adjusted chance levels; branch values carry the observed range and excess beyond its nearest edge. One question remains, and it is not about any individual candidate: given how many things were tested, should we believe this one?