Selection
Selection answers one question: which stored runs are eligible for this analysis?
It answers it entirely from storage keys and git topology. No excluded object is ever fetched or read — which is what keeps analysis fast against a store holding years of history, and why the stage exists as a stage at all.
Terms used here
| Term | What it means |
|---|---|
| merge base | The newest commit a branch and its base still share. |
The funnel
flowchart TD
A["Every object in the store"] --> B["Discriminant filter<br/>engine · target triple · machine key"]
B --> C["On the analyzed history?<br/>first-parent ancestry of the context"]
C --> D["Dirty admission<br/>which side of the merge base?"]
D --> E["--since cutoff<br/>by committer date"]
E --> F["Sort by key, assign ordinals"]
F --> G["Fetch and parse only these"]
The funnel below traces one worked query — a host-local analyze — against this store. Two of its inputs drive the removals that would otherwise look arbitrary: the discriminant filters resolve to this host’s own partitions, so a foreign partition measured on another machine is dropped; and --since resolves to a cutoff date part way through the history, dropping everything committed before it. Each row is one selection stage and what it removed.
| Stage | What it removes from this store | Objects removed | Still eligible |
|---|---|---|---|
| Every run object in this store | 54 | ||
| Discriminant filter | every object of criterion / macos-arm64 / 7c6d, a partition the query’s discriminant filters do not name | 20 | 34 |
| On the analyzed history | runs recorded at commits that are not on the context’s first-parent line | 3 | 31 |
| Dirty admission | 1 dirty run, recorded at or before the merge base (commit 19) | 1 | 30 |
--since | everything still eligible that is older than commit 6 | 8 | 22 |
| Fetched and parsed | 22 |
32 runs removed and 22 runs account for all 54 run objects this store held. Only those survivors are fetched and parsed; every other run was decided on its storage key and the commit’s place in the topology alone.
The store in this worked example holds only runs — stored benchmark measurements (see Shape of the data). A blessing, a recorded acceptance of a change (see Reconstruction), is a separate object kind stored alongside them: it is set apart during discriminant selection and follows its own path, so it never enters the run-only topology, dirty-admission, and window stages counted here.
The grid draws the analyzed first-parent line, so the runs the on-history stage removed have no column in it — having no place on that line is exactly why they were removed. Partitions are labeled by engine, target triple, and machine key, with the triples shortened to fit.
Target, base, and the split
Two refs frame every analysis:
- the context (
--context, defaultHEAD) whose history is analyzed, and - the base (
--base, default the detected default branch).
The tool walks the context’s first-parent ancestry and splits it at the merge base with the base. Everything at or before the merge base is base-side; everything after is target-side.
flowchart LR
b0["·"] --> b1["·"] --> MB["merge base"]
MB --> t1["·"] --> T["context = HEAD"]
MB -. "base branch continues" .-> B["base tip"]
subgraph baseside["base-side"]
b0
b1
MB
end
subgraph targetside["target-side"]
t1
T
end
Everything in the left box is base-side (at or before the merge base); everything in the right box is target-side (after it).
If the base cannot be resolved, or shares no ancestor with the context, that is a hard
error — not a silent fallback. The usual cause is a shallow CI clone; fetch full history, or
pass an explicit --base.
Mode is derived, not chosen
From the shape of the history it is handed, selection derives an analysis mode: the question the rest of the pipeline will answer about the series. There are two, and later stages — detection above all — branch on the one selection chose:
- History mode — does a level move somewhere in an official branch’s own history?
- Branch mode — is the analyzed context commit off the base it would merge into?
There is no flag to force the choice. The tool reads two signals:
| Context commit equals the merge base | Dirty run admitted on the context commit | Mode |
|---|---|---|
| yes | no | history |
| yes | yes | branch |
| no | no | branch |
| no | yes | branch |
The second signal is the surprising one. A run recorded from a dirty working tree on top of the base tip selects branch mode, because that is what it is: work that is not in the base’s history yet, sitting on top of it. An unnamed feature branch.
Note it is the admitted run that decides this, not the state of your checkout. A dirty checkout with no dirty run in the analysis still analyzes as history.
Dirty admission
A dirty run is one measured with uncommitted changes. Where it is allowed depends on which side of the merge base it sits:
| Side | Clean runs | Dirty runs |
|---|---|---|
| Base-side (at or before the merge base) | admitted | rejected |
| Target-side (after the merge base) | admitted | admitted |
| The base tip itself | admitted | admitted, but only while the tree is currently dirty |
Why base-side dirty runs are rejected: the base is the reference the analysis compares against. A measurement of somebody’s uncommitted experiment is not a description of the base’s history, and letting it in would corrupt the baseline for everyone.
--no-dirty removes dirty runs from the target side as well, so no dirty run is admitted
anywhere — the base side already rejects them regardless.
The window
--since drops whole runs older than a cutoff, judged by each commit’s committer date.
The default is where the two modes diverge, and it is worth knowing which you are in:
- History mode applies a six-month look-back by default, so a scheduled trend watch does not silently widen as history accumulates.
- Branch mode applies no default. A branch is judged against its base’s recent commits, and a time window could only starve that comparison of evidence.
The cutoff is deliberately one-sided. There is no --until, because --context already
anchors the newest edge of the timeline — a topology-first tool moves the tip by naming a
commit, not by naming a date.
A
--sinceshorter than your collection cadence will quietly turn a healthy store into a report full of unjudged series. The report’s coverage line is where that shows up.
Discriminant filters
Three discriminant filters restrict which discriminant sets participate: --engine,
--target-triple, and --machine-key. Each is repeatable and unioned.
Omitting one auto-detects from the host — except --engine, which defaults to all of them,
since a machine can produce output from several engines at once. The literal all disables a
filter entirely.
Because auto-detection means the same command can search different data on different machines, every query prints a one-line effective selection summary to stderr, naming what was actually searched and which parts were defaulted. It is not opt-in.
Ordering is fixed before any parallelism
Runs that survive the preceding selection stages are sorted by storage key and assigned their positions before any object is fetched. Loading is then parallel, but the result is not affected by which worker finished first.
Two analyses of the same store therefore produce byte-identical reports. That is what makes a report diffable and a regression in the tool itself detectable.
What the other commands share
analyze, list runs, and examine run this same selection pipeline, so list genuinely
previews the selected raw data. The columns below name concepts that later
chapters introduce — benchmark prefixes, the
ghost filter, and
configured exclusions and
blessings — so a linear reader is not expected to know them yet;
they are forward references. The divergences are real, though, and worth knowing:
| Discriminant filters, window, dirty rules | Benchmark prefixes | Ghost filter | Configured ignores | Blessings | |
|---|---|---|---|---|---|
analyze | yes | yes | yes | yes | yes |
examine | yes | exact ID | no | no | no |
list runs | yes | no | n/a | no | n/a |
prune | its own rules | n/a | n/a | no | n/a |
examine deliberately skips ghosts, configured ignores and blessings: it is a drill-down onto raw
recorded data, and hiding points there would defeat its purpose. prune is a maintenance
command with its own selection rules — notably no default --since — because deleting data
by accident is much worse than analyzing too little.
Ignore matching needs benchmark IDs from the payload, so it happens after loading, not in this key-only selection stage. It leaves the selected run count unchanged.
A base merged into the branch is supported
Branch mode builds its comparison window from the base ref’s own recent first-parent history, anchored at the base ref. Whether the branch was rebased onto the base, the base was merged into the branch, or both happened during development does not change that comparison window.
The context commit’s first-parent line is still the line that list, examine, and prune
display or maintain, and it is still divided at its fork point — the newest commit it
shares with the base ref — into base-side history and the branch’s own commits. When the base
was merged in, the merge base sits off that line, so the fork point is the newest shared
commit rather than the merge base itself; base-side history is identified and preserved either
way, so prune never deletes a shared commit without the base-branch opt-in.
The merge base is consulted only as a shared-history check. If no merge base can be resolved at all, the repository checkout is too shallow or the refs are disjoint, and selection stops with the existing merge-base-unavailable error.
What selection hands on
An ordered set of eligible objects, and the topology needed to place them. Next: Reconstruction.