Define the claim to test.
A microbenchmark can estimate throughput, compare implementations under a shared local schedule, or expose unstable behavior. It cannot establish end-to-end service latency, production capacity, or a universal result for another host.
The crate uses mach_absolute_time on Darwin and CLOCK_MONOTONIC_RAW on Linux through a narrow C clock bridge. Adaptive calibration aims for useful batch durations while retaining the raw per-operation sample means and the clock characterization needed to audit them.
Measure one statically bound operation.
flyology_bench is currently version 0.1.1-dev and is published through the Flyology organization index. Keep the community index for compiler dependencies, add the Flyology development index ahead of it, then add the crate normally.
alr index --reset-community
alr index --add=git+https://github.com/flyology-ada/alire-index.git \
--name=flyology --before=community
alr with flyology_bench
with Flyology_Bench;
with Flyology_Bench.Reporters;
with Interfaces;
procedure Bench_Mix is
use type Interfaces.Unsigned_32;
Value : Interfaces.Unsigned_32 := 1;
procedure Mix is
begin
Value := Value * 1_664_525 + 1_013_904_223;
end Mix;
procedure Run is new Flyology_Bench.Measure (Mix);
Result : Flyology_Bench.Measurement;
Config : constant Flyology_Bench.Configuration :=
Flyology_Bench.Reporters.Terminal_Mode (Name => "integer mix");
begin
Run (Config => Config, Result => Result);
Flyology_Bench.Reporters.Put_Console ("integer_mix", Result);
end Bench_Mix;
The Flyology_Bench package stores output in a Measurement and applies a Configuration. Terminal_Mode prepares terminal reporting, and Put_Console writes the result.
Measure is generic, so the operation is statically bound. Use Measure_Batched when one timed batch coordinates tasks or owns a fixture that the harness must not recreate per operation. Use setup and teardown hooks when preparation must stay outside each timed interval. The generated flyology_bench API reference lists the complete configuration, measurement, comparison, and reporter contracts.
Read the measurement report.
Terminal mode keeps labels and numeric columns stable during progress, then prints one row per kind of evidence. The capture below is from the maintained example on Darwin/AArch64. Results will change with the host, compiler, load, and run policy.
-- integer_mix ----------------------------------------- latency | median 1.498 ns/op mean 1.633 ns tails | p95 2.225 ns p99 2.784 ns range 1.454 ns .. 2.839 ns sampling | 16777216 iter/sample x 55 samples median batch 25.13 ms quality | CV 18.60% outliers 7 lag-1 correlation 0.47 clock | mach_absolute_time resolution | nominal 42.000 ns observed 41.000 ns median read 41.000 ns floor 0.003 ps/op cpu | ▇▇▇▇▇▇▇▇▁▇▇▇▇▇▇▇▇▇▇▇▇▇▁▇▇▇▇▇▇▇▇▇▇▇▇▇▁▇▇▇▇▇▇▇▇▆▃▄ | average 96.8% (1.0 cores) peak 100.0% (1.0 cores) memory | ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ | RSS change across own batches +0.0 KiB largest batch +0.0 KiB elapsed | 00:00:01.5 timed samples (hh:mm:ss)
- Latency
- Median and mean of the retained per-operation batch averages. Prefer neither blindly; inspect their separation and the quality row.
- Sample tails
p95andp99describe batch averages, not the latency distribution of individual operations.- Quality
- Coefficient of variation, retained Tukey outliers, and lag-one correlation expose dispersion and sample-order structure.
- Resolution
- Nominal and observed clock steps, adjacent-read cost, and the step divided by batch iterations show the timer scale behind the estimate.
Choose balanced rounds or sequential blocks.
Do not infer a comparison by running implementation A for a minute and implementation B later without recording that schedule. Compare alternates which of two implementations runs first. Compare_Many accepts two to sixteen enumeration values and, by default, rotates every implementation through each execution position.
type Candidate is (Current, Rewrite, Tasked);
procedure Batch
(Which : Candidate;
Iterations : Flyology_Bench.Iteration_Count);
procedure Compare_All is new Flyology_Bench.Compare_Many
(Case_Id => Candidate, Batch => Batch);
procedure Put_All is new
Flyology_Bench.Reporters.Put_Multi_Comparison_Console (Candidate);
Result : Flyology_Bench.Multi_Comparison;
Config : Flyology_Bench.Configuration :=
Flyology_Bench.Reporters.Terminal_Mode (Name => "parser shootout");
begin
Config.Measurement_Time := 3.0;
Config.Maximum_Sampling_Time := 2.0;
Config.Comparison_Batching := Flyology_Bench.Equal_Time;
Config.Shootout_Scheduling := Flyology_Bench.Sequential_Cases;
Compare_All (Config => Config, Result => Result);
Put_All (Result, Show_Individual_Details => True);
The batch receives an Iteration_Count. The generic Put_Multi_Comparison_Console renders a Multi_Comparison.
Equal_Time- The default. Every implementation calibrates its own iteration count toward an equal share of the timed budget, so faster cases do more operations rather than receiving less observation time.
Shared_Iterations- Every implementation receives one logical count. Use it for stateful batches that require identical work; elapsed collection time then varies with implementation speed.
Balanced_Rounds- The default shootout schedule. Cases are interleaved and rotate through execution positions to limit time-order drift.
Sequential_Cases- One case completes its sample block before the next starts. Telemetry is easier to attribute visually, but thermal and time-order drift can bias separated blocks.
The first enumeration value is the reference. A speedup above 1.0x means the contender is faster. The elapsed-time column says the same thing in direct language: less is favorable, more is unfavorable. Color reinforces that verdict in a terminal, but the words and signs carry the meaning independently.
| Implementation | Median | Speedup | Elapsed time | 95% CI | Verdict |
|---|---|---|---|---|---|
| one mix | 1.792 ns | 1.000x | reference | reference | reference |
| two mixes | 3.358 ns | 0.537x | 86.24% more | [0.519, 0.558] | reference faster |
| noisy one mix | 1.776 ns | 0.977x | 2.37% more | [0.886, 1.072] | inconclusive |
| parallel CPU burn | 0.878 ns | 1.984x | 49.59% less | [1.850, 2.154] | contender faster |
The confidence interval comes from deterministic circular-block resampling of paired log-ratios. A practical verdict appears only when the complete interval clears the configured effect threshold. Show_Individual_Details appends the full latency, sampling, clock, CPU, memory, and elapsed card for every implementation.
Sweep exact parameters without changing the timed operation.
Flyology_Bench.Sweeps retains an explicit ordered set of positive size: or count: points. Each canonical point identity contains the parameter kind and full unsigned decimal value; an optional bounded label is display metadata. Duplicate identities and invalid labels fail before collection.
The sweep generic selects a point and states its work outside the timed batch, then invokes an already-instantiated Measure, Compare, or Compare_Batched procedure once. The operation inside that procedure stays statically bound. A paired sweep therefore performs the existing adjacent, order-balanced comparison independently at every point; it does not compare two later blocks.
- Work identity
- Every logical operation carries an exact positive integral amount in items, bytes, or a caller-named unit. Console work amounts and rates use the selected decimal or binary prefix; CSV retains decimal integer text, while newline-delimited JSON adds exact decimal-string companions to its numeric parameter, work, and scaling-range fields. A point skipped before setup or rejected while stating work carries no fabricated default amount.
- Throughput
- Operations per second and work units per second are derived by inverting the same wall nanoseconds-per-operation samples. There is no separately timed throughput measurement. Mean-time confidence endpoints are inverted in reverse order. Console and machine reporters retain the exact rate-availability state; paired rows do so independently for each side.
- Budgets and failures
- A per-point budget applies the runner limit independently. A whole-sweep budget includes selection, warmup, calibration, and collection and passes each point the remaining collection limit. Setup failure, measurement failure, unavailable wall time, overflow, dry run, and budget exhaustion remain distinct inspectable statuses; callers choose stop or continue. Collection and derived-rate availability remain separate, so a throughput-only overflow retains valid elapsed data and a paired verdict. A runner that raises after partially writing its output leaves reset default storage in the failed point.
- Suite identity
- A suite full case name is supplied as
Case_Nameand remains the registration and filter identity. Machine rows add a separate stablepointfield; the sweep layer does not create another suite runner.
Flyology_Bench.Scaling analyzes stored or deterministic synthetic observations after collection. One analysis has one coherent size or count parameter kind; mixing the two is rejected. It fits constant, logarithmic, linear, n log n, quadratic, and cubic candidates in log space and reports the parameter kind, every coefficient, nominal exponent, R-squared value, RMS and maximum log residual, selected model, and observed input range. Rejected nonempty data retains its factual range, while empty input reports the kind and range unavailable. A rejected analysis marks no diagnostic selected; when fitting reached model comparison, the API still retains the lowest-residual candidate for callers that inspect the rejection status first. Fewer than four distinct positive points, a range below twofold, invalid observations, numerical overflow, poor fit, or poor identifiability produce an explicit unavailable state. The twofold boundary is decided with exact integer arithmetic before floating-point fitting. The result is empirical scaling over the observed range, not proof of big-O.
cd flyology_bench
alr exec -- gprbuild -p -P examples/flyology_bench_examples.gpr
./examples/bin/sweep_comparison
The example compares insertion sort and Shell sort over five input sizes, prints median elapsed time, confidence bounds, operations per second, work units per second, and the paired verdict at every point, then analyzes each implementation's stored medians. It reports that invocation and makes no host performance claim.
Register and select a benchmark suite.
Flyology_Bench.Suites builds a bounded explicit registry. Ada provides no required reflection or linker-discovered inventory, so each case has a stable registered identity. The runner preserves registration order unless --order=name requests lexical full-identity order.
./examples/bin/suite_runner --list
./examples/bin/suite_runner --filter='integer/*' --skip='*comparison'
./examples/bin/suite_runner --exact=integer/mix --output-style=csv
./examples/bin/suite_runner --dry-run --output-style=json \
--output=suite.ndjson
Names, groups, tags, and matching are case-sensitive. A filter without * or ? is a substring match. A glob uses only those two portable wildcards. A no-match selection returns a failing status unless --allow-empty is explicit.
Every selected callback receives one shared Configuration. The callback wraps an existing generic Measure, Compare, or Compare_Many instantiation. Multi-way registration also binds the case enumeration to the existing generic reporters. Indirect dispatch occurs before collection, while the timed operation inside that instantiation remains statically bound.
- Serial process
- Cases execute serially. Global state, caches, and host conditions carry between cases.
- Exceptions
- The default continues after a callback exception and reports its full identity.
--fail-faststops before the next case. - Dry run
--dry-runinvokes each callback with ten time-only samples, a finite collection cap, and optional host gates, telemetry, scheduler probes, and progress disabled. Output is validation-only and contains no performance numbers.- Machine files
--outputwrites plain human output, typed CSV table sections, or newline-delimited JSON. Suite context prefixes the complete existing latency and metric schemas. Configured terminal progress and ANSI sequences stay outside machine output.
The aggregate reports discovered, selected, completed, skipped, failed, inconclusive, unavailable, and rejected counts. An inconclusive paired or multi-way comparison does not fail by default. The maintained main maps a failing final status to Ada.Command_Line.Failure.
Measure resources with the same sample schedule.
Wall time remains the calibration and collection-budget clock. The Metrics field holds a Metric_Set. The predefined Process_Resource_Metrics and Linux_Hardware_Metrics values select additional counters. The harness captures them immediately before and after every retained batch, so time, CPU, memory, operating-system activity, and scheduler results use the same sample schedule.
use type Flyology_Bench.Metric_Set;
Config.Metrics :=
Flyology_Bench.Process_Resource_Metrics
or Flyology_Bench.Linux_Hardware_Metrics;
- CPU and memory
- Process CPU includes every native thread. Thread CPU covers the pthread executing the batch. RSS is process-wide (operating systems do not provide meaningful per-thread RSS), so absolute RSS is diagnostic while across-batch RSS change describes retained growth.
- OS activity
- Minor and major faults, voluntary and involuntary context switches, actual storage bytes, and filesystem block-I/O operations are reported per logical operation.
- Linux PMU
- CPU cycles, retired instructions, IPC, cache misses, branches, and branch misses use user-space
perf_event_open. Cycles and instructions share a synchronized event group. Counters inherit into native tasks or processes created by the executing pthread after initialization, but they do not attach to threads that already existed. Each sample subtracts a baseline read from a final read while the group is disabled; one enable interval between those reads gives cycles and instructions a common start. The implementation does not reset counters because reset clears an event's own count but retains inherited counts from child tasks that have exited. - Comparison method
- Positive pairs use relative percent changes. Signed or zero-containing axes use paired absolute differences. Diagnostic axes retain intervals without declaring a generally favorable direction.
An unavailable axis reports why: unsupported platform, permission denial, unsupported event, unavailable counter resources, or probe failure. The API exposes the same value through Metric_Status. Console, CSV, and JSON output preserve the value instead of substituting zero.
Custom axes and alternate timing
Custom_Metric_Registry adds up to eight caller-defined axes in deterministic registration order without extending Metric_Axis. Each descriptor fixes a bounded stable name and unit, scope, attribution, direction, cumulative-delta or explicit absolute/completed-elapsed semantics, per-batch or per-operation normalization, and relative-positive or absolute comparison. Duplicate names, built-in collisions, invalid metadata, resets, non-finite values, and conversion overflow are rejected or retained as statuses; unavailable samples are never replaced with zero.
The provider callback runs immediately before and after each retained batch. Built-in begin probes precede the custom begin; the harness wall timestamp then surrounds the batch; the custom end precedes built-in ending probes. This ordering is reported rather than corrected for cross-probe perturbation. Providers declare attribution explicitly. A thread-scoped provider must detect migration or otherwise establish that both boundaries used the same native thread.
Flyology_Bench.Manual_Timing and Manual_Timing_Comparison are static generic adapters for caller-completed elapsed values. The batch must synchronize device, accelerator, or simulated work before returning. Harness wall time remains a distinct axis and remains the clock for warmup, equal-wall calibration, collection limits, interference windows, and progress. Wall timestamp-cost subtraction is never reused for the alternate source. Each invocation owns its adapter state and composes with an existing custom provider while consuming one registry slot. Reported resolution is normalized by the retained iteration count, with distinct reference and contender resolutions after unequal calibration. Compact console output places the primary timer first with its source and resolution, then labels harness wall as calibration; paired output identifies the primary axes-table verdict. The maintained example uses a deterministic simulated clock and does not claim GPU support.
The versioned extended CSV and NDJSON reporters identify built-in versus custom axes, timing role and source, output unit and resolution, calibration clock, semantics, attribution, status, summaries, paired method, interval, and verdict. Existing CSV schemas remain unchanged. Custom providers are not part of Recording: overlapping and migrating spans require a separate bounded concurrency and ownership design before per-span custom attribution can be coherent.
perf_event_open returns EINVAL for an unsupported event and for a rejected attribute combination. The harness repeats that probe with only permission-related attributes before it classifies the result. This check distinguishes a missing counter from a harness defect.
All probes and counter controls stay outside the wall-clock timestamps, and setup or teardown hooks stay outside every selected axis. Probe families can still perturb one another at their boundaries. Adaptive batches amortize that fixed work, but a small resource difference should be repeated with only the relevant metric family selected.
Flyology scheduler axes include dispatches, poll batches, poll events, wakeups, and migration boundary crossings. They use Scheduler_Probe. A library-level adapter can copy cumulative values from one or more Flyology.Observability.Group_Snapshot records. The callback runs outside the timed interval, and the standalone benchmark crate keeps no dependency on the Flyology runtime.
Set a wall-time limit for collection.
Measurement_Time is the target amount of timed work across all cases. Maximum_Sampling_Time is a hard wall-time budget for the collection phase. A value of zero disables the limit.
Config.Measurement_Time := 3.0;
Config.Maximum_Sampling_Time := 2.0;
Config.Samples := 75;
The harness always completes at least ten samples and checks the budget only between complete samples, pairs, balanced rounds, or sequential case samples. One in-flight batch can therefore finish slightly after the boundary. A sequential shootout divides the collection ceiling among cases and analyzes the common sample count all cases completed. In every comparison, the measurement target is divided across timed implementations instead of being multiplied by their count.
Wait for sustained low CPU load.
CPU_Quiescence is an optional preflight check. Before clock characterization and warmup, it samples host CPU counters. The host average and busiest logical CPU must remain within their limits for one continuous interval. The check runs once for each measurement, comparison, or multi-way shootout.
Config.CPU_Quiescence :=
(Enabled => True,
Maximum_Average_CPU_Percent => 20.0,
Maximum_Core_CPU_Percent => 50.0,
Stable_Time => 1.0,
Poll_Interval => 0.100,
Timeout => 15.0);
The terminal progress bar represents the accepted portion of Stable_Time. It returns to zero when either utilization limit is exceeded. If the complete stable interval is not observed before Timeout, the benchmark raises CPU_Quiescence_Timeout without starting warmup or timed collection.
- Host average
- Catches load spread across several logical CPUs. A low average alone can hide one saturated core on a large machine.
- Busiest core
- Rejects an interval when any logical CPU exceeds
Maximum_Core_CPU_Percent. - Stable interval
- Requires consecutive accepted samples. A single quiet poll is not sufficient.
- Timeout
- Bounds preflight wall time. The poll interval must not exceed it, and the timeout must cover the requested stable interval.
The maintained example enables the preflight with FLYOLOGY_BENCH_QUIESCENCE=1. It remains disabled by default so unattended runs do not acquire a new wait or failure condition.
Check power profiles and thermal conditions.
Operating_Conditions selects an independent, opt-in Operating_Conditions_Policy. The policy can observe:
- configured power profiles and low-power mode;
- thermal state and available hardware throttle counters; and
- process-profile changes that the collector sees at probe boundaries.
The harness runs every condition probe outside timed regions.
Config.Operating_Conditions :=
Flyology_Bench.Pause
(On_Pause_Timeout => Flyology_Bench.Fallback_Observe,
Require_Nonreduced_Profile => True,
Require_Profile_Detection => False,
Maximum_Thermal_State => Flyology_Bench.Thermal_State_Fair,
Require_Thermal_Detection => False,
Window => 0.050,
Stable_Time => 0.500,
Poll_Interval => 0.100,
Maximum_Pause_Time => 30.0,
Rewarm_Time => 0.050);
- Activation
Disabledis the default. Constructing the policy withObserve,Pause, orFailopts in and names the response directly. The policy is private, so an aggregate cannot enable it with an omitted response.PauserequiresOn_Pause_Timeout; there is no implicit expiration action.- Profile rules
Require_Nonreduced_Profilerejects a reduced configured profile or enabled low-power mode.Require_Profile_Detectionmakes missing profile evidence unacceptable.- Thermal rules
Maximum_Thermal_Statesets the highest acceptable thermal state from nominal through critical. The example usesThermal_State_Fair. Unknown is an observation result, not an acceptable threshold.Require_Thermal_Detectionmakes absence of every supported thermal-state, throttle-counter, and degradation signal unacceptable.- Collection window
Windowsets the minimum target duration for each complete group of collection units.- Recovery polling
Stable_Timesets the required continuous recovery interval.Poll_Intervalcontrols probe spacing.- Pause budget
Maximum_Pause_Timeis one cumulative condition-wait budget for the complete run. When that budget expires,On_Pause_Timeoutselects the action.- Collection recovery
Rewarm_Timesets the workload rewarm duration after conditions recover during collection.
Disabled- The default performs no operating-condition probes and adds no new wait or failure condition.
Observe- The harness records unacceptable evidence and continues. During collection, it retains the affected collection window. Use this response when a run must finish without a new wait or failure condition.
Pause- The harness can wait at initial preflight, after workload warmup, and after calibration. If conditions recover during the calibration check, the harness repeats calibration. During collection, it waits for continuous recovery, rewarms, and recollects the complete affected window. Pause and rewarm time do not consume the sampling budget.
Fail- During collection, the harness completes the affected window before it raises
Operating_Conditions_Unacceptable. At initial, post-warmup, or calibration checks, the harness raises the exception at the check boundary. Condition_Pause_FallbackFallback_Observerecords the expired budget and continues. During collection, it retains the original affected window. At earlier checks, no collection window exists.Fallback_Failraises the same exception.
A collection unit is one sample, one comparison pair, or one balanced multi-way round. A response does not split a collection unit.
The policy groups complete collection units according to calibrated duration. If the operating-condition watch and the interference watch described next are both enabled, the harness uses the larger window target. A final window can contain fewer units.
- macOS
The collector reads power mode and power source from
pmset. It reads live low-power, thermal-pressure, and process-profile values from publicNSProcessInfoAPIs. A Default or Sustained process profile is acceptable by itself. If the collector observes a change from the post-warmup profile at a probe boundary, it records a transient event.macOS thermal pressure reports system stress. It is not a public hardware throttle counter. At each probe boundary, the collector reads live
NSProcessInfovalues. The collector force-refreshespmsetat preflight, after warmup, after calibration, at the terminal sampling-window close, and on entry toPause. Ordinary collection-window openings, intermediate closes, and subsequentPausepolls reuse a one-second coarse cache while liveNSProcessInfosampling continues. Final reporting reuses the last already-judged snapshot. If a brief profile switch ends before the next boundary, the collector does not observe it.- Linux
The collector resolves the configured profile in this order:
- It queries an already-running power-profiles-daemon and reads its degradation reason.
- If that profile is unavailable, it checks modern
/sys/class/platform-profilehandlers. If every present handler is readable and all values agree, the collector accepts their common value. - If no modern handler is present, the collector reads the legacy ACPI
platform_profilefile.
If a modern handler is unreadable or conflicts with another handler, the profile remains unavailable. The collector does not use the legacy file in that case.
When the kernel exposes Intel-style thermal-throttle event and duration counters, the collector reads them separately.
Opening the local system bus is synchronous and is outside the D-Bus method-call deadline. A stalled connection can extend the nominal probe and pause bounds. After connection, the collector applies the remaining absolute deadline to D-Bus calls. Condition-probe time remains outside sampling timestamps and the sampling budget.
The counters are cumulative history, not a live throttle-state signal. An event count increases when throttling begins, while cumulative duration is updated after that episode ends. A counter increase proves that throttling occurred during the observation span, but flat counters cannot prove that an in-progress episode cooled. After observing an event,
Pausetherefore remains unresolved until its cumulative budget applies the configured fallback.
Environment (Result) retains:
- detector identities and initial, final, and worst states;
- profile changes and throttle deltas; and
- affected and recollected units, pause time, and fallback use.
JSON preserves structured availability and detector fields for enabled runs. Console output is a compact summary and does not distinguish an unavailable throttle total from zero. Existing CSV schemas remain unchanged.
Watch for CPU interference during collection.
The operating-condition watch does not measure foreign CPU load. The CPU quiescence preflight establishes only that the host was quiet before warmup. Interference adds an optional watch during collection. It estimates foreign load between timed samples, never inside them.
Config.Interference :=
(Enabled => True,
Response => Flyology_Bench.Retake,
Maximum_Foreign_CPU_Percent => 10.0,
Window => 0.050,
Maximum_Retakes => 25,
Settle_Time => 0.250,
Maximum_Pause_Time => 30.0,
Rewarm_Time => 0.050);
Without placement, foreign load is host busy time minus this process's own CPU time. The preflight can compare raw host utilization against a limit, because the harness is idle at that point. During collection the benchmark thread saturates a core, so an unsubtracted limit would trip on every sample. Load that the benchmark itself creates is this process's own CPU time, and is therefore not foreign.
Observe- The default. Records the observation and keeps every sample. The raw distribution stays exactly as collected.
Retake- Discards the contaminated window and collects it again.
Maximum_Retakesbounds the total for one run. Use it for brief bursts of load. Pause- Waits for the host to settle, re-warms the workload, then collects the window again. Use it for sustained load.
Rewarm_Timesets the untimed warmup after the wait. A resumed run has cold caches and frequency state, so a zero warmup leaves the first sample as the outlier that the pause avoids. Window- Host CPU counters are tick-based. A window shorter than this duration is recorded, but never acted on. Below roughly one tick, the estimate is mostly quantization. A window also covers whole units, so a window that covers many short samples can exceed
Maximum_Retakeson its first discard. Collection then continues underObserveand reports the exhausted budget.
A window spans whole collection units: one sample, one comparison pair, or one balanced multi-way round. A response therefore never splits a pair. It never lands inside a round, where it would spread interference unevenly across the cases that the round compares.
When a retake or pause budget runs out, collection continues under Observe and the report records that. Paused time is excluded from Maximum_Sampling_Time, because waiting for the host is not collection.
Placement pins the benchmark thread. It also narrows attribution to the placed CPU and its SMT siblings. The harness then measures only those CPUs, and subtracts the placed thread's CPU time rather than the whole process. Keep Include_Siblings enabled. A process that saturates the SMT sibling of the placed CPU slows the measurement, while the placed CPU's own share stays clean.
Linux applies strict affinity. Darwin's affinity API is an advisory tag, and Apple Silicon implements no thread affinity at all. Attribution therefore stays host-wide on Darwin, unless Require_Strict makes that an error instead.
Placement binds only the calling thread. Another thread of the same process can therefore occupy a watched CPU, where its time is indistinguishable from foreign load. Each window reads process CPU alongside thread CPU. The difference bounds the share of the watched capacity that those threads can account for. When that share exceeds Maximum_Foreign_CPU_Percent, the run continues host-wide and reports Attribution_Diluted. A scheduler benchmark under placement therefore reports a host-wide number, instead of discarding samples over load that it created itself.
Host_Lock claims host CPU capacity for the whole run. It coordinates with any tool that follows the host CPU lock convention, including load generators and profilers. The claim uses /tmp/host-cpu.lock by default. Path selects another file for one run, and HOST_CPU_LOCK_PATH selects one for every tool that follows the convention. The claim matters most for Pause. Two harnesses that both pause on interference each treat the other as foreign load, so they alternate between pausing and resuming instead of settling.
A claim is not proof of exclusivity. A privately mounted path reduces the claim to one mount namespace, and the report records that scope. systemd PrivateTmp= produces such a path routinely.
Environment returns every observation, and the console, CSV, and JSON reporters all carry it. The report states how many samples the harness discarded or collected again. Without those counts, a heavily repaired result would suggest a quieter machine than the run actually had. The maintained example enables the watch with FLYOLOGY_BENCH_INTERFERENCE=1. The watch, placement, and the host claim are all disabled by default, so an existing run acquires no new wait or failure condition.
Monitor the process outside timed intervals.
Terminal_Mode enables process telemetry and names every live progress line. During multi-way collection, a fixed-width field identifies the implementation that produced each sample. Its fixed width prevents the progress display from moving. The line also reports CPU percentage, occupied cores, current RSS, RSS growth, and elapsed wall time. Final reports retain CPU and RSS sparklines.
- CPU percentage
100%is approximately one occupied core. A tasking benchmark may exceed it; the example's four-worker case reaches roughly four cores.- Memory
- The labeled
shootout totalfooter reports absolute process RSS. Individual implementation cards report RSS change observed around that implementation's own batches. - Elapsed
- Individual cards label timed sample duration. The shootout footer labels total wall time. Both use
hh:mm:ss. - Probe boundary
- Progress CPU and RSS reads occur outside timed regions and describe the whole run. Selected measurement axes are also probed outside the timestamps but retained per batch and normalized per logical operation where applicable.
Record work controlled by the application.
Flyology_Bench.Recording is for a server, consumer, worker pool, or other long-lived process whose own control flow decides when work starts. A bounded Recorder stores identities of type Benchmark. Call Register before Start, then mark each span inside the application.
Begin_Sample initializes a Span. Finish it with the Success or Failure outcome. The benchmark runner remains available for calibrated microbenchmarks; recording does not force request handling through a harness callback.
package Recording renames Flyology_Bench.Recording;
Recorder : Recording.Recorder
(Maximum_Benchmarks => 8,
Retained_Samples => 10_000);
Request : Recording.Benchmark;
Recording.Register (Recorder, "request", Request);
Recording.Start (Recorder);
declare
Sample : Recording.Span;
begin
Recording.Begin_Sample (Recorder, Request, Sample);
begin
Handle_Request;
exception
when others =>
Recording.Finish (Sample, Recording.Failure);
raise;
end;
Recording.Finish (Sample, Recording.Success);
end;
Registration allocates the bounded stores. The span path performs no Ada heap allocation. Finish captures its ending timestamp before entering the protected store, so retention work is outside recorded wall time. A snapshot reports observed, retained, omitted, active, abandoned, successful, failed, timed out, and cancelled spans. Each retained row keeps its observation number and outcome aligned with the status and optional value of every requested axis.
- Individual spans
- Recorded p50, p95, and p99 describe individual application spans. They are not the runner's per-operation batch means.
- Independent comparison
Compare_Independentresamples separately observed distributions. It does not reuse the runner's paired bootstrap or claim correlation that the load schedule did not establish.- Partial samples
- If only some retained spans have an axis, its status is
partially_collectedand valid and unavailable counts are reported separately. Raw rows remain aligned, but an independent comparison uses an axis only when both inputs are complete. - Bounded retention
Reservoirkeeps a bounded sample across the complete session.First_NandLatest_Nare available when the beginning or end is the intended window. Aggregate lifecycle and outcome counts still cover every completed span.- Attribution
- Wall time has exact span boundaries. Thread CPU requires the same native thread at both boundaries. Process CPU, RSS, faults, switches, and I/O include concurrent process work. Linux PMU values use recorder-owned per-worker groups and retain native task-tree scope; worker exit closes its group.
The live terminal rewrites one fixed display instead of printing an event log. It shows elapsed time, process CPU, occupied cores, and RSS. Each registered identity has one row for completed spans, active spans, errors, rolling median, and rolling p95.
fly recorder elapsed 01.31 s cpu 38.4% / 0.4 cores rss 31.2 MiB
benchmark done live errors median p95
quick request 66 0 2 42.0 ns 250.0 ns
cpu-heavy request 68 1 1 3.21 ms 4.08 ms
memory-burst request 70 0 2 140.3 us 358.2 us
wait-bound request 64 1 2 19.01 ms 64.03 ms
cd flyology_bench
alr exec -- gprbuild -p -P examples/flyology_bench_examples.gpr
./examples/bin/recording_service
FLYOLOGY_BENCH_RECORDING_OUTPUT=csv ./examples/bin/recording_service
FLYOLOGY_BENCH_RECORDING_OUTPUT=json ./examples/bin/recording_service
The example does not use an HTTP server. Eight client tasks drive a long-lived rendezvous service with four workers. Instrumentation lives inside those workers, while fast, CPU-heavy, memory-burst, wait-bound, occasional-failure, and occasional-hiccup work makes the live fields move. CSV and JSON label the contracts as sample_semantics=individual_span and comparison_design=independent. Raw CSV and the JSON samples array retain observation identity and outcome across axes. Comparison output retains unavailable axes; when wall time is absent, wall-derived JSON fields are null rather than a fabricated 1× result.
Emit CSV or JSON without terminal output.
CSV and newline-delimited JSON reporters omit progress and ANSI sequences. The maintained example selects the reporter through one environment variable.
FLYOLOGY_BENCH_OUTPUT=terminal ./examples/bin/basic
FLYOLOGY_BENCH_OUTPUT=csv ./examples/bin/basic > results.csv
FLYOLOGY_BENCH_OUTPUT=json ./examples/bin/basic > results.ndjson
./examples/bin/basic --metrics=perf --require-perf
./examples/bin/basic --metrics=perf --require-perf=core
The existing CSV tables keep their latency schemas. Put_Metrics_CSV, Put_Comparison_Metrics_CSV, and the multi-way long-form reporter emit one row per requested axis with scope, unit, availability status, summaries, confidence bounds, and verdict. JSON measurement, comparison, and multi-way objects include the same status and metric arrays alongside clock and host/toolchain metadata. Flyology_Bench.Baselines records one named raw-sample artifact and gates a later compatible run.
identity='cpu=ci-runner-1;policy=cpu-2;switches=-O3;benchmark=v1'
./examples/bin/baseline_gate record build/integer_mix.baseline "$identity"
./examples/bin/baseline_gate check build/integer_mix.baseline "$identity"
FLYOLOGY_BENCH_OUTPUT=csv \
./examples/bin/baseline_gate check build/integer_mix.baseline "$identity"
FLYOLOGY_BENCH_OUTPUT=json \
./examples/bin/baseline_gate check build/integer_mix.baseline "$identity"
Record mode is explicit. Check mode never updates its reference, including after rejection. The writer flushes a unique same-directory temporary file before an atomic POSIX rename. A failure before publication leaves the earlier artifact intact. The version 2 artifact has exact benchmark and environment identities, the clock backend, raw time samples, a checksum, and a commit footer. The reader also accepts the earlier public version 1 format without silently upgrading it; version 1 has no checksum or commit footer. Raw times must lie within the range the harness can produce, from one nanosecond divided by the maximum iteration count through one complete unsigned 64-bit clock delta. Out-of-range artifacts fail with a format diagnostic. An invalid current measurement becomes a gate error with no partial statistics, so fail-closed CI rejects it without an escaping arithmetic exception. The example combines its required caller identity with the default OS, architecture, and GNAT fingerprint. Include the stable CPU or runner class, placement policy, compiler switches, and benchmark contract rather than a changing contender revision.
- Regression
- The gate rejects only when the complete 95% confidence interval establishes a slowdown beyond the practical threshold. It does not reject a noisy point estimate.
- Other verdicts
- Improvement, practical equivalence, and inconclusive remain distinct. Policy decides whether an inconclusive result rejects the run.
- Artifact policy
- Interactive policy can report a missing, invalid, or incompatible artifact. CI can reject those states. The implementation never compares different benchmark names, fingerprints, or clock backends.
- Output
- Console, CSV, and newline-delimited JSON retain status, rejection, compatibility, speedup and time-change intervals, threshold, bootstrap method, confidence level, resample count, seed, and reason. Confidence-neutral machine field names prevent a configured interval from being mislabeled. A suite can count each status and use the rejection field for its final process status.
Use a direct paired comparison when both implementations can run in one process. Pairing retains the balanced collection schedule. The saved-baseline gate uses the existing independent-run bootstrap because its runs occurred at different times. Its bounded sum, mean, ratio, interpolation, time-change, and interval-verdict primitives form a private SPARK unit whose floating run-time checks are proved by the repository proof suite; file I/O and bootstrap orchestration remain behaviorally tested boundaries. One artifact represents one named reference. Longer histories, dashboards, commit-range analysis, and change-point detection remain separate work.
Use fresh processes to isolate process-global state.
Flyology_Bench.Workers invokes the same executable directly through posix_spawn. Each worker executes one exact ordinary or paired case. A paired comparison stays inside one worker, so reference and contender retain their shared order and sample schedule.
cd flyology_bench
alr exec -- gprbuild -p -P examples/flyology_bench_examples.gpr
./examples/bin/fresh_process
Each repetition performs its own host setup, warmup, calibration, and timed sampling. Keep the returned worker results separate. If you compute an across-worker interval, resample workers rather than flattening their internal samples. Spawn and setup durations are metadata outside per-operation time.
Strict environment mode keeps only execution and temporary-directory variables by default. Locale and timezone require an explicit preservation policy, and worker results retain the effective mode and both policies. The reported environment fingerprint covers variable names but deliberately excludes their values, so a shared report cannot be used to test guesses for inherited credentials. For the internal exact-value check, the worker computes a separate digest and returns it only through the dedicated result channel; it is absent from process arguments and public results. The parent captures standard output and error separately with bounded retention and exact omitted-byte counts. Startup and total deadlines terminate the anchored process group, apply a bounded grace period, and reap the root process.
The caller must retain exclusive child-reaping ownership while a worker run is active. Detected ownership loss fails as parent I/O and disarms the PID guard, but a concurrent external reaper violates the API contract and can race the observation-to-reap interval. Successful envelopes are accepted only when their raw samples reproduce the reported statistics and their telemetry, host-control, interference, and metric metadata match the requested configuration. Inactive and unavailable sections must remain canonically empty.
Treat fractional nanoseconds as amortized throughput.
A result such as 0.878 ns/op does not mean the clock directly resolved one operation in 878 picoseconds. The harness timed a batch lasting milliseconds and divided that duration by the logical operation count. This is useful throughput evidence when the batch performs exactly that many comparable operations.
- Clock precision
- The native timestamp and observed-step rows characterize the timer. Dividing its step by millions of iterations explains the reported arithmetic floor; it does not improve the physical clock.
- Timer subtraction
- Timestamp-cost subtraction is off by default. If enabled, one observed adjacent-read interval is removed from each batch, never an invented empty-loop cost.
- Outliers
- Every sample remains in the statistics and bootstrap. Outlier counts are prompts to investigate host noise, workload phases, or inputs, not permission to delete inconvenient data.
- Comparison scope
- Balanced order limits one source of drift. It cannot control frequency scaling, thermal state, interrupts, other processes, or a benchmark that mutates unequal state.
Repeat on the host you intend to describe.
The crate is maintained on Darwin and Linux, including native Linux/AArch64 Docker validation. Its default fingerprint records operating system, architecture, and GNAT version. Add compiler switches, revision, CPU placement and power policy, and other conditions that could change the result.
cd flyology_bench
alr test
./examples/bin/basic
./examples/bin/fresh_process
alr test also checks the published machine-readable schemas. Every CSV row must contain the columns declared by its header. Long-form metric rows must agree between available and status. Each JSON object must parse. The test uses jq when available and reports when it uses structural checks instead.
Fresh-process workers use Darwin's close-on-exec spawn policy or glibc Linux's close-from spawn action. An unavailable extension fails the launch instead of using fork. Windows remains unsupported.
From the repository root, FLYOLOGY_LINUX_PERF=1 ./scripts/test-linux-docker.sh grants Docker's PERFMON capability and makes the example require real hardware-counter samples. It also makes the smoke test compare a serial batch against one that starts four worker tasks, so inherited attribution is measured rather than assumed. A virtualized Linux host must also expose its PMU; the capability alone cannot provide missing virtual hardware. A guest kernel without one lists no CPU entry under /sys/bus/event_source/devices and rejects every hardware event, and the run then reports the specific unavailable status instead of passing.
For a shared report, keep the raw machine output, record the exact command and environment, repeat the full run, and describe only the tested host. Direct Compare or Compare_Many is preferable whenever all implementations can run in one process.