Benchmark short Ada operations.

flyology_bench adaptively batches short Ada operations and compares up to sixteen implementations. Reports include the clock, sampling, and host context needed to interpret nanosecond-scale results. A fractional nanosecond result is an amortized batch value, not a directly observed interval.

BENCH 01

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.

Warmexercise code and data before collection
Calibratechoose a batch large enough for the native clock
Sampleretain complete per-operation batch means
Analyzereport variation, confidence, and order effects

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.

BENCH 02

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.

add the development index and crate
alr index --reset-community
alr index --add=git+https://github.com/flyology-ada/alire-index.git \
  --name=flyology --before=community
alr with flyology_bench
bench_mix.adb
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.

BENCH 03

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 · observed example output
-- 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
p95 and p99 describe 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.
BENCH 04

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.

three implementations, one shared shootout
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.

ImplementationMedianSpeedupElapsed time95% CIVerdict
one mix1.792 ns1.000xreferencereferencereference
two mixes3.358 ns0.537x86.24% more[0.519, 0.558]reference faster
noisy one mix1.776 ns0.977x2.37% more[0.886, 1.072]inconclusive
parallel CPU burn0.878 ns1.984x49.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.

SWEEPS

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_Name and remains the registration and filter identity. Machine rows add a separate stable point field; 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.

run the maintained paired size sweep
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.

SUITE

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.

discover and validate the maintained suite
./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-fast stops before the next case.
Dry run
--dry-run invokes 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
--output writes 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.

BENCH 05

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.

request portable resource and Linux hardware axes
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.

BENCH 06

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.

a three-second target with a two-second ceiling
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.

BENCH 07

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.

require one quiet second, but wait no longer than fifteen
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.

BENCH 08

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.

pause until conditions recover, then recollect the affected collection window
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
Disabled is the default. Constructing the policy with Observe, Pause, or Fail opts in and names the response directly. The policy is private, so an aggregate cannot enable it with an omitted response. Pause requires On_Pause_Timeout; there is no implicit expiration action.
Profile rules
Require_Nonreduced_Profile rejects a reduced configured profile or enabled low-power mode. Require_Profile_Detection makes missing profile evidence unacceptable.
Thermal rules
Maximum_Thermal_State sets the highest acceptable thermal state from nominal through critical. The example uses Thermal_State_Fair. Unknown is an observation result, not an acceptable threshold. Require_Thermal_Detection makes absence of every supported thermal-state, throttle-counter, and degradation signal unacceptable.
Collection window
Window sets the minimum target duration for each complete group of collection units.
Recovery polling
Stable_Time sets the required continuous recovery interval. Poll_Interval controls probe spacing.
Pause budget
Maximum_Pause_Time is one cumulative condition-wait budget for the complete run. When that budget expires, On_Pause_Timeout selects the action.
Collection recovery
Rewarm_Time sets 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_Fallback
Fallback_Observe records the expired budget and continues. During collection, it retains the original affected window. At earlier checks, no collection window exists. Fallback_Fail raises 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 public NSProcessInfo APIs. 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 NSProcessInfo values. The collector force-refreshes pmset at preflight, after warmup, after calibration, at the terminal sampling-window close, and on entry to Pause. Ordinary collection-window openings, intermediate closes, and subsequent Pause polls reuse a one-second coarse cache while live NSProcessInfo sampling 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:

  1. It queries an already-running power-profiles-daemon and reads its degradation reason.
  2. If that profile is unavailable, it checks modern /sys/class/platform-profile handlers. If every present handler is readable and all values agree, the collector accepts their common value.
  3. If no modern handler is present, the collector reads the legacy ACPI platform_profile file.

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, Pause therefore 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.

BENCH 09

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.

collect a contaminated window again instead of keeping it
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_Retakes bounds 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_Time sets 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_Retakes on its first discard. Collection then continues under Observe and 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.

BENCH 10

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 total footer 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.
RECORDER

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.

record one application-owned request
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_Independent resamples 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_collected and 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
Reservoir keeps a bounded sample across the complete session. First_N and Latest_N are 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.

recording_service · live display
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
run the self-contained service example
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.

BENCH 11

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.

human and script output from the same benchmark
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.

record and check one durable baseline
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.

BENCH 12

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.

compare in-process and fresh-process modes
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.

BENCH 13

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.
BENCH 14

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.

build, test, and run the maintained example
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.