fixture design
Design goal
The same input must reach every implementation, and the cost of setting it up
must be either inside or outside the measurement by declaration, not by
accident. fixture separates the five things that happen to an input
(generate, identify, copy, prepare, reset) so that the runner can place each
of them on the right side of the clock.
Mathematical background
A fixture is a small state machine applied per dataset and implementation :
Two equations express the contract the runner relies on.
- Isolation. If an implementation mutates , the next preparation must still see the original : must not share mutable state with . Then every batch starts from the same state, and the measured workload is the same function of in every block.
- Determinism. is a function of the context alone, so equal contexts give equal inputs and equal fingerprints: .
The runner checks neither; violating them makes blocks measure different workloads.
Setup cost in the measurement
Let one operation cost and one preparation . For a batch of operations the runner reports , where
SetupFrequency | IncludedInMeasurement | ExcludedFromMeasurement |
|---|---|---|
PerIteration | ||
PerSample, PerBatch | ||
| long-lived | (the one preparation lands in an early, discarded batch) |
Including setup per batch measures an amortized cost that depends on the batch size chosen by calibration; include it only per iteration, or when the batch size is fixed.
Design decisions
Closures in a record, not a trait
Problem. Fixtures differ in input type, prepared type and policy, and are
often written inline in a test. Choice. Fixture is a record of functions
with three type parameters. Why. A trait would need one type per fixture
and could not carry an id, a version and a policy as values.
Clone before prepare
The runner always calls clone_input before prepare. Fixture::immutable
makes both the identity, which is free for values that are never mutated; a
mutable input supplies a real copy. Keeping clone and prepare apart lets a
fixture copy the input once and build a workspace around it, and makes the
cost of each visible.
Implementation-aware preparation
prepare receives the implementation id, so it can produce the layout an
implementation expects (packed, transposed, padded). The preparation is then
part of the fixture, and its timing follows the declared policy rather than
hiding inside one implementation’s payload.
Policy as data
SetupPolicy is recorded with every observation (setup_timing) and in the
event stream. A reader can tell a cold-start measurement from a warm one
without reading code.
Correctness and invariants
materializeandfingerprintrun once per dataset.preparealways receives a freshclone_inputresult.- Every short-lived prepared value is reset exactly once; long-lived values are reset at the runner’s checkpoints and reused (see the runner design).
Fixture::immutableuses identity functions and a no-op reset.
Alternatives rejected
- Letting implementations copy their own input. The copy would be timed for some implementations and not for others.
- One setup hook. Could not distinguish generation (once per dataset) from preparation (per batch or iteration).
Boundaries
- The fixture does not enforce isolation or determinism; it states the contract.
WorkspaceScopeis descriptive; the runner keys its cache per implementation and dataset.PerRunandPerDatasetare prepared per implementation, likePerImplementation.- The fixture does not measure memory.