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.0-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.

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.

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

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 09

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. For regression history, Flyology_Bench.Baselines stores raw time samples and rejects a comparison when the clock backend or environment fingerprint differs.

BENCH 10

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 11

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

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.

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.