Performance Evidence Guide¶
Use this guide when a build-cost claim needs evidence that another learner or reviewer can inspect without reconstructing a shell loop. The route measures local behavior. It does not define a universal speed threshold.
Start with the question¶
make perf answers:
What do the governed clean, converged no-op, and converged dry-run routes cost in this named environment and revision, and did each route preserve convergence, trusted outputs, and the declared incremental rebuild scope?
It does not answer:
- whether the current revision regressed without a compatible earlier bundle
- whether a small delta matters operationally
- which recipe lies on the critical path
- whether timings from another machine are directly comparable
Produce one evidence bundle¶
From capstone/, run:
Use PERFORMANCE_SAMPLES only when the experiment contract calls for another repetition
count:
Name a stable execution environment when evidence may be compared later:
The label is not decoration. It distinguishes environments that a platform string may not, such as a laptop run and a CI runner with the same operating system. Keep one label for one stable environment; do not rename a changed machine to make evidence compare.
The minimum is three. Five is the default because one timing is not a baseline and a very large local sample count would imply precision this teaching route does not provide.
Read the bundle in order¶
The bundle is written under:
Read:
route.txtfor the question and limitsscenarios.tsvfor the exact state, job count, and route contractincremental-policy.tsvfor every declared input class and expected impactincremental-summary.tsvfor the observed change-impact matrixincremental-work.jsonfor per-case traces, missing work, and unexpected workevidence.jsonfor provenance, the bound incremental matrix, every timing sample, and trusted-output inventoriessummary.tsvfor min, median, max, spread, and convergencelogs/for command output and exit status from each timing samplemanifest.jsonfor the bundle member inventory
This order is deliberate. A median is interpretable only after you know what ran, where it ran, and whether it did the same work.
Read execution context as evidence¶
The context object in evidence.json records:
| Field | Why it belongs in the claim |
|---|---|
| repository revision | identifies the source and Make graph measured |
| repository status and status digest | exposes uncommitted source state |
| resolved GNU Make command and version | identifies the evaluator and scheduler |
| resolved compiler command and version | identifies the main clean-build recipe tool |
| Python version | identifies the evidence collector runtime |
| platform | provides the operating-system and machine boundary |
| runner label | names the stable execution environment chosen by the operator |
Collection records a dirty worktree rather than hiding it. Comparison rejects dirty or unavailable repository provenance because two timings cannot establish a revision-to-revision change when either side includes unidentified source edits.
Different revisions are expected in a useful comparison. The report names both revisions; it does not require them to match. The compiler, GNU Make, Python, platform, runner label, scenario policy, and sample count do have to match. If one changes, recollect or describe a cross-environment observation without calling it a controlled performance comparison.
Understand the scenarios¶
| Scenario | State established before each sample | Route | Teaching purpose |
|---|---|---|---|
clean-build |
documented clean route |
parallel all |
observe full production cost |
converged-noop |
all succeeds and make -q all passes |
serial all |
observe an up-to-date invocation |
converged-dry-run |
all succeeds and make -q all passes |
serial -n all |
localize cost before ordinary recipe execution |
Dry-run is not pure parse timing. It includes evaluation and graph decisions, and recursive Make recipes have special dry-run behavior.
Why the inventories belong beside timing¶
After every scenario, the collector requires make -q all to pass and inventories:
allappbuild/bin/dyn1build/bin/dyn2build/include/dynamic.h
A candidate that omits work may be faster. It is not comparable if those trusted outputs change unexpectedly or the route stops converging.
Final bytes are not enough. The incremental route executes a change-impact matrix:
| Input advanced | Must rebuild | Must remain unchanged | Edge class under review |
|---|---|---|---|
src/util.c |
build/util.o, app, all |
other objects and dynamic pipeline | direct source dependency |
include/util.h |
build/main.o, build/util.o, app, all |
sub object and dynamic pipeline | depfile-discovered shared header |
include/sub.h |
build/main.o, build/sub/sub.o, app, all |
util object and dynamic pipeline | depfile-discovered shared header |
scripts/gen_dynamic_h.py |
generated header, both dynamic binaries, all |
application and ordinary objects | generated producer fan-out |
src/dynamic/dyn1.c |
build/bin/dyn1, all |
application, ordinary objects, sibling binary, generated header | independent dynamic source |
Every row classifies the same governed output universe. That completeness rule matters. If an output appeared in neither column, the runner would not observe it and a false pass would be possible.
For each row, the runner:
- converges
all - snapshots every governed output timestamp
- advances only the declared input beyond all outputs
- executes
make --trace all - restores the input timestamp even when the case fails
- classifies observed, missing, and unexpected rebuilds
The next case starts from a converged build, not from the previous case's artificial input timestamp. Passing cases remain in the report when a later case fails, so reviewers can see the exact stopping evidence without losing earlier observations.
Missing work exposes stale-output risk. Unexpected work exposes an overbroad graph. The two failures lead to different repairs: restore a required edge versus narrow an unnecessary one.
The collector copies the passing semantic fields into evidence.json. This binding matters:
the comparator does not look for a conveniently named sibling file and hope it belongs to the
timings. It compares the policy digest, complete input set, and each case's expected and
observed rebuild sets carried by both evidence documents.
Run that proof alone with:
Compare two compatible bundles¶
Preserve the earlier bundle outside current/, because the next make perf replaces that
directory. Then run:
The comparison rejects:
- different scenario-policy digests
- different sample counts
- different GNU Make, compiler, Python, platform, or runner identities
- dirty or unavailable repository provenance
- different scenario sets, states, routes, or job counts
- failed evidence
- failed convergence
- changed trusted-output inventories
- different incremental policy or input-case set
- different expected or observed work in any case, including a non-first case
- missing or unexpected incremental rebuilds
It deliberately permits different clean revision identifiers and records both in
execution_context. Only after all compatibility and semantic checks pass does it report
median deltas. The report does not label a negative delta a “win.” Compare the delta with
both observed spread and a stated operational objective.
Review the gate itself¶
Run:
The tests prove that:
- resolved tool and repository identity are captured
- clean, dirty, and unavailable repository states remain distinguishable
- every policy case classifies one complete governed output set
- later failing cases remain attributable and restore their inputs
- missing cases and non-first-case rebuild drift are rejected
- a complete governed capture passes
- fewer than three samples are rejected
- a failed command produces failed evidence
- compatible bundles can be compared
- context, policy, sample-count, result, artifact, and requested-work drift are rejected
This matters because a gate that has only seen passing input is not yet trustworthy.
Make an honest decision¶
Use one of these outcomes:
| Evidence | Defensible decision |
|---|---|
| one bundle, no prior compatible evidence | baseline established; regression unproven |
| compatible bundles, delta inside observed spread | no material change established |
| compatible bundles, objective exceeded, semantics match | investigate or accept the measured change |
| comparison rejected for toolchain or runner drift | evidence describes different environments |
| comparison rejected for dirty provenance | commit or restore the intended source state, then recollect |
| comparison rejected for requested-work drift | timing claim blocked until the graph change is explained |
| comparison rejected for artifact drift | performance claim blocked by semantic change |
| comparison rejected for contract drift | recollect under one experiment contract |
Write down what remains unknown. A local experiment can support a local engineering decision without pretending to be a portable benchmark.
Companion routes¶
make trace-reportfor bounded clean-plan causality and attribution evidencemake selftest-reportfor convergence and schedule-equivalence proofmake incident-auditfor verified process and artifact signatures across concurrency specimensmake profile-auditfor tool, platform, and variable-origin boundaries