Benchmark Guide

Linux::Event keeps permanent benchmark harnesses for the reactor, public IO/Kernel resources, ordered-byte engine, framing, TLS, process I/O, and release-regression decisions. Older one-off experiments live under bench/archive/ and are not the recommended release gates.

Some script filenames predate the 0.110 public taxonomy. Names such as run-stream-*, run-wakeup-*, and run-datagram-* are retained engineering tool names so historical result sets remain comparable. They do not make Linux::Event::Stream, Linux::Event::Wakeup, or Linux::Event::Datagram public application classes. Current application code uses Linux::Event::IO::* and Linux::Event::Kernel::*.

Some low-level benchmark rows also deliberately instantiate retained private implementation hosts to measure native-engine overhead. Those rows are implementation baselines, not API examples.

Release performance regression

run-performance-regression.pl is the permanent release gate. Capture the baseline and candidate on the same idle machine, with the same Perl build and system configuration:

perl Makefile.PL && make
perl -Mblib bench/run-performance-regression.pl \
  --json bench/results/performance-baseline.json

Then build the candidate and compare:

perl -Mblib bench/run-performance-regression.pl \
  --baseline bench/results/performance-baseline.json \
  --threshold-percent 10 \
  --fail-on-regression \
  --json bench/results/performance-candidate.json

The full suite uses seven repeats and covers reactor registration, timer lifecycle/expiration, ordered-byte lifecycle and throughput, native framing, deadline-enabled ordered-byte throughput, and stream-socket connect/listener lifecycle. It reports wall throughput and process CPU cost. A baseline with a different benchmark contract/configuration is rejected.

Use --quick only while developing the harness. Release decisions should use the full default workload.

Public-surface overhead

run-public-api-overhead.pl is repository engineering tooling used to compare retained private implementation hosts with the 0.110 public leaves on the same build. It is intentionally excluded from the CPAN distribution.

Its purpose is to catch overhead introduced by semantic wrappers such as IO::Pipe and IO::Sock::Stream; it is not a user-facing API benchmark. Large 200 KB stream-socket payloads are measured with a dedicated longer run so tiny sample sizes cannot decide the median.

Reactor comparison

run-reactor-comparison.pl compares the low-level reactor with equivalent poller/readiness APIs in other event systems. This is deliberately separate from the high-level stream-socket comparison.

Typical reference run:

ulimit -n 100000
perl bench/run-reactor-comparison.pl --build \
  --systems linuxevent,ev,anyevent-ae,uv,ioasync-epoll,mojo-epoll \
  --clients 1000,5000,10000,20000 \
  --warmup 1 \
  --messages 100 \
  --bytes 64 \
  --client-workers 4 \
  --repeats 6 \
  --timeout 180 \
  --out bench/results/reactor-comparison.html \
  --json bench/results/reactor-comparison.json

The fairness contract keeps TCP loopback, connection setup, registration, warmup, payload size, request/reply behavior, correctness checks, teardown, and execution-order rotation comparable across systems.

Stream-socket competitor comparison

run-stream-competitor-comparison.pl implements the high-level comparison contract in STREAM-COMPETITOR-PLAN.md. It compares Linux::Event::IO::Sock::Stream constructor closures with AnyEvent::Handle, UV::TCP, IO::Async::Stream, and Mojo::IOLoop::Stream. Every row uses the framework's normal buffered Stream write API. The common client workers verify the exact response bytes before the measured server loop stops.

Check the optional modules and selected backends first:

perl bench/run-stream-competitor-comparison.pl --build --check-deps

Publish raw and delimiter-framed rankings as separate result sets:

ulimit -n 100000
perl bench/run-stream-competitor-comparison.pl --build \
  --workload raw --clients 100,500,1000,2500 \
  --warmup 10 --messages 100 --bytes 64 \
  --client-workers 4 --repeats 5 --timeout 90 \
  --out bench/results/stream-competitor-raw.html \
  --json bench/results/stream-competitor-raw.json

perl bench/run-stream-competitor-comparison.pl \
  --workload delimiter --clients 100,500,1000,2500 \
  --warmup 10 --messages 100 --bytes 64 \
  --client-workers 4 --repeats 5 --timeout 90 \
  --out bench/results/stream-competitor-delimiter.html \
  --json bench/results/stream-competitor-delimiter.json

Run on the same idle host and publish the companion JSON with each HTML report. The default five repeats rotate every system through every execution position. Do not combine raw and delimiter rows into one ranking. Framework buffering and backpressure are part of the measurement because they are part of what an application actually uses. The harness still writes diagnostic reports when a case fails, but exits nonzero and excludes that row from the ranking summary.

For payload sensitivity, repeat both commands with --bytes set to 16, 4096, and 65536. Treat 200 KB as a dedicated longer run (more messages or repeats), not as one row in the ordinary short-message run.

Ordered-byte payload sweep

run-stream-payload-sweep.pl measures established ordered-byte receive performance across the permanent 64 B through 200 KB matrix. The historical filename refers to the shared native ordered-byte engine; interpret application results through IO::Pipe, IO::TTY, or IO::Sock::Stream as appropriate.

perl -Mblib bench/run-stream-payload-sweep.pl \
  --modes=raw,delimiter --repeats=5 --warmup=1 \
  --variant=baseline --commit=COMMIT_SHA \
  --output=bench/results/stream-payload-sweep.json

The report records receiver/producer CPU, throughput, native reads/writes, Perl callback counts, buffering peaks, and effective ordered-byte/socket settings. This is a saturated one-way local throughput benchmark, not a latency test.

Ordered-byte lifecycle and transition diagnostics

The following harnesses target specific private engine properties while public classes remain IO::Pipe, IO::TTY, and IO::Sock::Stream:

These are implementation diagnostics. Do not copy private class names from a benchmark row into application code.

First-class callback regression

The first-class callback harnesses preserve the production invariants established by the cached-closure experiment:

Use repeated paired runs. For the accepted-connection harness, parent process CPU per accept is more reliable than wall throughput because client scheduling affects the latter. The fresh-closure row is diagnostic; production Listener callbacks are shared.

perl -Mblib bench/run-first-class-callback-construction-bench.pl \
  --clients=100 --connections=10000 --repeats=9 \
  --json=bench/results/first-class-callback-construction.json

The raw and framed harnesses retain small-message dispatch diagnostics plus larger convergence rows. They are regression checks, not a request to reopen the already-answered question of whether lexical capture is intrinsically slow.

Callback batching

run-callback-batching-microbench.pl compares ordinary raw/framed callbacks with explicit raw-byte and framed-message batching:

perl -Mblib bench/run-callback-batching-microbench.pl \
  --messages=1000000 --bytes=64 --read-size=4096 \
  --raw-batch-bytes=0,16384,65536,262144 \
  --message-batch-sizes=0,1,4,16,32,64,256 \
  --transports=unix,tcp --warmup=1 --repeats=7 \
  --json=bench/results/callback-batching.json

Zero selects ordinary callback behavior. Partial batches flush at the current native drain boundary; they do not wait for future readiness merely to fill a configured batch.

run-callback-batching-fairness.pl places a timestamped fixed-frame probe next to a continuously readable ordered-byte workload and reports hot-path throughput plus probe p50/p99/maximum latency:

perl -Mblib bench/run-callback-batching-fairness.pl \
  --duration=0.5 --ping-interval-us=2000 --bytes=64 --read-size=4096 \
  --batch-sizes=0,16,32,64 --transports=unix,tcp \
  --warmup=1 --repeats=5 \
  --json=bench/results/callback-batching-fairness.json

This is a deliberate saturation/fairness diagnostic rather than ordinary network latency.

Framing

run-framing-microbench.pl and run-native-framers-microbench.pl measure the built-in framing families and native parser path.

run-framer-send-bench.pl measures the complete framed outbound path: Perl prefix encoding, native submission/queueing, Loop-driven writev draining, and peer receipt.

perl -Mblib bench/run-framer-send-bench.pl \
  --framers=length,varint --repeats=5 --warmup=1 \
  --variant=candidate --commit=COMMIT_SHA \
  --output=bench/results/framer-send.json

Framing is an ordered-byte capability and applies to public Pipe, TTY, and stream-socket leaves.

Native consumer lifetime

run-async-consumer-lifetime-bench.pl exercises the external Linux::Event::Async native consumer against the core ordered-byte consumer ABI. Use paired core/Async builds in PERL5LIB:

PERL5LIB=/path/to/async/blib/lib:/path/to/async/blib/arch:\
/path/to/core/blib/lib:/path/to/core/blib/arch:$PERL5LIB \
perl bench/run-async-consumer-lifetime-bench.pl \
  --repeat=5 --warmup=1 --variant=candidate \
  --core-commit=CORE_SHA --async-commit=ASYNC_SHA \
  --output=bench/results/async-consumer-lifetime.json

The benchmark is a saturated local throughput/lifetime diagnostic. It does not claim request/response latency percentiles.

Timer

run-timer-microbench.pl isolates the shared timerfd/indexed-heap scheduler used by public Linux::Event::Kernel::Timer objects:

perl -Mblib bench/run-timer-microbench.pl \
  --counts=1000,10000,100000 \
  --repeats=5 \
  --cpu-clock=auto \
  --json=bench/results/timer-microbench.json

It measures attach/cancel lifecycle, rescheduling, and equal-deadline expiration. Use the release regression suite for release gating and this harness for Timer-specific scaling diagnosis.

Signal

run-signal-microbench.pl measures public Linux::Event::Kernel::Signal delivery and native fan-out at increasing subscriber counts:

perl -Mblib bench/run-signal-microbench.pl \
  --deliveries=10000 --subscribers=1,10,100 --repeats=5

The benchmark uses one-at-a-time real-time signals so it measures the Loop/signalfd round trip rather than queue saturation.

Event

run-wakeup-microbench.pl retains its historical filename but measures the eventfd engine exposed publicly as Linux::Event::Kernel::Event:

perl -Mblib bench/run-wakeup-microbench.pl \
  --signals=100000 --batch-sizes=1,16,256 --repeats=5 \
  --json=bench/results/wakeup-microbench.json

Batch size one measures a full signal-to-Loop round trip; larger batches expose eventfd coalescing. Logical signals and Perl callbacks are reported separately.

Resolver

run-resolver-microbench.pl measures the private asynchronous hostname resolver worker/eventfd service:

perl -Mblib bench/run-resolver-microbench.pl \
  --host=localhost --requests=1000 --repeats=5

Use a stable local or controlled DNS target. Public internet resolver latency is environment noise and is not a release gate.

Dgram

run-datagram-microbench.pl retains its historical filename and measures public Linux::Event::IO::Sock::Dgram connected and unconnected UDP echo:

perl -Mblib bench/run-datagram-microbench.pl \
  --packets=100000 --bytes=64 --modes=connected,unconnected --repeats=5 \
  --json=bench/results/datagram-microbench.json

Every payload is checked and only one request is outstanding, making packets/second and CPU cost comparable across modes.

Process

run-process-microbench.pl measures public Linux::Event::Kernel::Process spawn/pidfd/reap/on_exit lifecycle:

perl -Mblib bench/run-process-microbench.pl \
  --program=/bin/true --processes=1000 --concurrency=1,8,32 --repeats=5 \
  --json=bench/results/process-microbench.json

run-process-pipe-drain-bench.pl isolates native stdout/stderr pipe draining:

perl -Mblib bench/run-process-pipe-drain-bench.pl \
  --engines=perl,native --streams=stdout,stderr,both \
  --workers=1,8,32 --read-sizes=4096,65536 \
  --bytes-per-stream=16777216 --warmups=1 --repeats=7 \
  --json=bench/results/process-pipe-drain.json

The perl engine is a retained benchmark reference only; native is the production path. --heartbeat-us=1000 adds a recurring Kernel::Timer fairness probe under saturated process output.

Listener lifecycle

run-listen-microbench.pl measures inbound stream-socket acquisition:

perl -Mblib bench/run-listen-microbench.pl \
  --clients=1,10,100 \
  --connections=10000 \
  --repeats=9 \
  --timeout=30

The public rows exercise Linux::Event::IO::Sock::Listener and construct the same minimal IO::Sock::Stream subclass for accepted connections. A manual row provides an explicit socket/Loop->watch/Perl accept baseline. Execution order rotates across repeats.

The benchmark uses abortive client close deliberately so very large short-lived TCP matrices do not turn later rows into ephemeral-port/TIME_WAIT tests.

Outbound stream-socket connection lifecycle

run-connect-microbench.pl measures outbound acquisition through public IO::Sock::Stream->connect() against a raw nonblocking socket baseline:

perl -Mblib bench/run-connect-microbench.pl \
  --clients=1,10,100 \
  --connections=10000 \
  --repeats=6 \
  --timeout=30 \
  --json=bench/results/connect-lifecycle.json

Connection setup and teardown are timed; established-message throughput belongs to the ordered-byte/TLS benchmarks. The timeout is a catastrophic per-request safeguard rather than a benchmark-row duration.

TLS

run-tls-microbench.pl compares established plain and OpenSSL transport paths for public IO::Sock::Stream subclasses. It deliberately excludes construction and handshake from the timed message interval.

run-tls-accept-setup-bench.pl isolates accepted-connection TLS setup. The fresh_context row constructs and loads a server SSL_CTX for every simulated connection. The prepared_clone row prepares one Listener-style server context and then allocates only independent per-connection TLS state:

perl -Mblib bench/run-tls-accept-setup-bench.pl   --iterations=1000 --repeats=7   --json=bench/results/tls-accept-setup.json

The benchmark reports the one-time prepared-context cost separately and does not time TLS handshake. Together the two TLS harnesses distinguish deployment setup cost from steady-state encrypted I/O cost.

Use the same certificate fixtures, OpenSSL build, Perl build, socket-buffer settings, and host state when comparing reports.

Benchmark decision records

Release-impacting performance decisions should preserve raw evidence and a short rationale under bench/decisions/. BENCHMARK-DECISIONS.md is engineering history and is intentionally excluded from CPAN distribution artifacts.

A useful decision record includes:

Do not compare results from different hosts as though they were a controlled before/after benchmark.