Skip to content

Commit a14f1d0

Browse files
committed
TASK-198: Benchmark installed native protocols with verified live receipts
Add opt-in installed public H1/H2/H3 benchmarks with independent clients, bounded connection epochs, monotonic samples, response validation, server counters and immutable provenance. Document per-protocol workloads without inventing performance thresholds. All 20 full TLS-on/off shared/static managed/external cells passed with 81920 measured completions and zero unexpected errors. Fifteen benchmark contracts, installed public H2/H3 gates, unchanged resource checks, offline revalidation and distribution checks passed. Record the approved unchanged file-size lint baseline and CI-owned nonlocal checks. Leave task tracking In Progress for caller-owned validation and finalization.
1 parent aef6089 commit a14f1d0

14 files changed

Lines changed: 2957 additions & 10 deletions

‎.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,8 @@ __pycache__/
150150
!test/installed/v3_http3_qpack.py
151151

152152
!test/installed/v3_http3_flow.py
153+
154+
# TASK-198 opt-in benchmark tools and contracts.
155+
!scripts/run-v3-native-benchmarks.py
156+
!scripts/test-v3-native-benchmark.py
157+
!test/installed/v3_benchmark_client.py

‎Makefile.am‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,18 @@ EXTRA_DIST += specs/architecture/v3/TASK-196-design.md \
575575

576576
EXTRA_DIST += specs/architecture/v3/TASK-197-design.md \
577577
specs/architecture/v3/TASK-197-implementation-evidence.md specs/architecture/v3/TASK-197-receipt.json
578+
579+
# Full workloads require explicitly selected installed packages and client tools.
580+
.PHONY: bench
581+
bench:
582+
@test -n "$(BENCH_PREFIX)" -a -n "$(BENCH_PROVIDER_PREFIX)" -a -n "$(BENCH_OUTPUT)" -a -n "$(BENCH_TLS)" || { echo "Set BENCH_PREFIX, BENCH_PROVIDER_PREFIX, BENCH_OUTPUT, BENCH_TLS (yes/no)"; exit 1; }
583+
$(BENCH_PYTHON_COMMAND) "$(top_srcdir)/scripts/run-v3-native-benchmarks.py" \
584+
--source "$(abs_top_srcdir)" --build "$(abs_top_builddir)" \
585+
--prefix "$(BENCH_PREFIX)" --provider-prefix "$(BENCH_PROVIDER_PREFIX)" \
586+
--output "$(BENCH_OUTPUT)" --tls "$(BENCH_TLS)" --compiler "$(CXX)" \
587+
--client-python "$(BENCH_PYTHON_COMMAND)" $(BENCH_ARGS)
588+
BENCH_PYTHON_COMMAND = python3
589+
EXTRA_DIST += scripts/run-v3-native-benchmarks.py scripts/lib/v3_native_benchmark.py \
590+
scripts/test-v3-native-benchmark.py test/installed/v3_benchmark_consumer.cpp \
591+
test/installed/v3_benchmark_client.py docs/native-v3-benchmarks.md \
592+
specs/architecture/v3/TASK-198-evidence.md specs/architecture/v3/TASK-198-receipt.json

‎docs/native-v3-benchmarks.md‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Native v3 packaged benchmarks
2+
3+
`make bench` is opt-in. It compiles an optimized C++20 server outside the source
4+
and build trees using the selected installed public headers and library. Independent
5+
Python clients run in separate processes. TLS-on requires all sixteen cells:
6+
shared/static × managed/external × H1 plaintext, H1 TLS, H2 TLS and H3 QUIC.
7+
TLS-off runs four plaintext cells and checks typed `not_supported` with no bound
8+
port for encrypted protocols. Unavailable protocols never count as measurements.
9+
10+
The measurement clients require Python 3 with hyper-h2 **4.4.1** and aioquic
11+
**1.3.0**. These are test dependencies only. Missing clients fail preflight.
12+
Select an OpenSSL 3.5 LTS provider explicitly. For example, from a fresh checkout:
13+
14+
```sh
15+
bash bootstrap
16+
python3 -m venv /tmp/native-benchmark-clients
17+
/tmp/native-benchmark-clients/bin/pip install h2==4.4.1 aioquic==1.3.0
18+
mkdir -p build/native-on
19+
cd build/native-on
20+
CXX=clang++-18 CC=clang-18 CXXFLAGS='-O2 -DNDEBUG' \
21+
V3_TLS_CFLAGS="-I$PROVIDER/include" \
22+
V3_TLS_LIBS="-L$PROVIDER/lib -lssl -lcrypto" \
23+
../../configure --enable-v3-tls=yes --disable-examples --prefix="$PREFIX"
24+
make -j4
25+
make install
26+
make bench BENCH_PREFIX="$PREFIX" BENCH_PROVIDER_PREFIX="$PROVIDER" \
27+
BENCH_TLS=yes BENCH_OUTPUT="$NEW_OUTPUT" \
28+
BENCH_PYTHON_COMMAND=/tmp/native-benchmark-clients/bin/python
29+
```
30+
31+
Repeat with a distinct fresh build/prefix/output and `--enable-v3-tls=no`,
32+
`BENCH_TLS=no`. Library and consumer optimization flags are recorded. The runner
33+
rejects nonoptimized libraries, sanitizer flags/symbols and allocator preload.
34+
Sanitizer measurements are correctness evidence and cannot enter comparisons.
35+
The direct entry point provides `--source`, `--build`, `--prefix`,
36+
`--provider-prefix`, `--tls`, `--compiler`, `--client-python` and `--output`.
37+
38+
The full workload has 4096 measured requests, 1024 response bytes and four
39+
server workers. H1/H2 concurrency is 8; H3 concurrency is 4. H1 warmup is 256.
40+
H1 uses eight persistent connections;
41+
H2 uses 64 connection epochs, each retaining one authenticated connection for
42+
eight warmup requests and 64 measured requests with eight concurrent streams:
43+
**512 actual warmup requests** and 4096 measured requests. The public default
44+
limits each H2 connection to 128 stream openings per second. Epochs stay below
45+
that allowance without changing production policy or adding artificial pacing.
46+
H3 has sixteen live-request seats and bounded lifetime stream records, so its
47+
full workload uses
48+
1024 connection epochs, each with four concurrent warmup requests followed by
49+
four measured streams: **4096 actual warmup requests** and 4096 measured requests.
50+
Every H3 epoch performs verified connection setup, completes warmup, reuses that
51+
connection for measurement, then closes it. The normal per-connection semantic
52+
data pool is capped at 393216 bytes; each active request reserves body and
53+
response buffers plus metadata. Concurrency 8 can produce H3_EXCESSIVE_LOAD
54+
refusals; concurrency 4 stays within the default admission capacity. This
55+
benchmark changes no production resource policy. Each epoch uses a new retained QUIC connection.
56+
The full cell deadline is 300 seconds, including setup and shutdown of client
57+
connections. Server startup and orderly shutdown each have an additional
58+
15-second bound. `--deadline` permits 1..600 seconds; other bounded overrides are
59+
`--warmup`/`--requests` (1..65536), `--concurrency` (1..8), `--h3-concurrency` (1..4, capped by
60+
`--concurrency`), `--payload` (1..16384) and `--workers` (1..32). H2/H3 epoch warmup follows concurrency, independent of the
61+
H1 warmup option. Overrides appear in the receipt; any deviation from defaults
62+
is labelled a reduced/custom workload rather than full acceptance evidence.
63+
64+
Timestamps are integer `time.monotonic_ns()` nanoseconds with clock resolution
65+
recorded. A request starts immediately before dispatch and ends at complete
66+
response receipt. Clients verify protocol negotiation, certificate trust and
67+
`a.example`, status 200, exact payload and run/request identity. Setup and all
68+
warmup samples are retained separately. Each measurement window starts after
69+
successful warmup and ends at its last measured completion. Duration is the sum
70+
of nonoverlapping measured windows in seconds; throughput is successful measured
71+
completions divided by that sum, in requests/second. For H2/H3 this excludes each
72+
epoch's handshake, warmup and close, and is **not whole-process wall throughput**.
73+
Whole-process wall seconds are also recorded. Differing protocol concurrency
74+
and connection reuse settings make these observations unsuitable for a direct
75+
protocol performance ranking. Latency percentiles use every
76+
successful measured sample without trimming: sort durations, select one-based
77+
rank `ceil(N*p/100)`, and display p50/p95/p99 in milliseconds. Raw request IDs,
78+
epoch membership and timestamps permit independent recomputation.
79+
80+
Each attempt must use a new output directory. Failures remain there. Receipts
81+
reference artifacts by absolute path, byte count and SHA-256, including raw samples,
82+
server observations, commands, executable, depfiles and link maps. Keep the
83+
recorded external consumer directory and package/provider/client installations
84+
alongside the receipt. Deleting or changing these invalidates verification. The
85+
server independently counts successful warmup/measured response writes and errors
86+
after orderly shutdown. Client exit zero alone is insufficient.
87+
88+
`validate(path, expected_identity, expected_run_id)` is available from
89+
`scripts/run-v3-native-benchmarks.py` for release adapter integration. Offline
90+
verification checks current source/package/build/workload identities and every
91+
artifact, then recomputes samples/statistics and compares server counts. A caller
92+
requiring new execution must obtain the fresh runner's run ID; revalidating an old
93+
receipt is integrity verification, not a new benchmark. CLI `--validate RECEIPT`
94+
accepts `--expected-run-id`. Hashes establish integrity against recorded inputs;
95+
fresh execution binds accepted evidence to the current run. They are not
96+
cryptographic attestation against an attacker controlling the producer and all
97+
artifacts. Synthetic parsing fixtures cannot substitute for a live attempt.
98+
99+
PRD §3.9 and the repair roadmap specify no absolute native throughput/latency
100+
minimum or comparable regression baseline. Historical `test/PERFORMANCE.md`
101+
v1/v2 numbers and unrelated 10×/2× claims do not apply. Acceptance requires full
102+
successful bounded measurements, exact counts, finite positive throughput,
103+
ordered finite percentiles, zero unexpected errors, and unchanged TASK-180
104+
resource gates: three output/upload saturation/reset/refill cycles, stalled
105+
observations >=0.99 seconds, CPU/wall <=0.2 of one core and warm RSS growth
106+
<=1,048,576 bytes. Run the existing normal `native_protocol_memory`,
107+
`resource_limits`, installed public H2/H3 drivers and 90-second resource probe
108+
separately; benchmark throughput does not replace their proof. Private resource
109+
fixtures remain supporting evidence. Nonlocal platform checks belong to CI/v3 PR.

‎scripts/lib/v3_native_benchmark.py‎

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""Bounded native benchmark measurement and immutable evidence validation."""
2+
from bisect import bisect_right
3+
import hashlib
4+
import json
5+
import math
6+
from pathlib import Path
7+
8+
UNITS = dict(timestamp='ns', duration='s', latency='ms', throughput='requests/s')
9+
DEFAULTS = dict(warmup=256, requests=4096, concurrency=8, payload=1024, workers=4, h3_concurrency=4, deadline=300)
10+
11+
12+
def require(value, message):
13+
if not value:
14+
raise ValueError(message)
15+
16+
17+
def parse_json(text):
18+
def pairs(items):
19+
result = {}
20+
for key, value in items:
21+
require(key not in result, 'duplicate JSON key: ' + key)
22+
result[key] = value
23+
return result
24+
def invalid(value):
25+
raise ValueError('nonfinite JSON number: ' + value)
26+
return json.loads(text, object_pairs_hook=pairs, parse_constant=invalid)
27+
28+
29+
def required_cells(tls):
30+
require(tls in ('yes', 'no'), 'unknown TLS mode')
31+
protocols = ('h1', 'h1-tls', 'h2', 'h3') if tls == 'yes' else ('h1',)
32+
return [(linkage, loop, protocol) for linkage in ('shared', 'static')
33+
for loop in ('managed', 'external') for protocol in protocols]
34+
35+
36+
def effective_concurrency(protocol, workload):
37+
return min(workload['concurrency'], workload['h3_concurrency']) if protocol == 'h3' else workload['concurrency']
38+
39+
40+
def epoch_workload(protocol, requests, concurrency, warmup):
41+
if protocol not in ('h2', 'h3'): return 1, warmup
42+
epochs = math.ceil(requests / (64 if protocol == 'h2' else concurrency))
43+
return epochs, epochs * concurrency
44+
45+
46+
def validate_cells(cells, tls):
47+
require(sorted(cells) == sorted(required_cells(tls)), 'missing, duplicate or unknown benchmark cells')
48+
49+
50+
def positive_integer(value):
51+
return type(value) is int and value > 0
52+
53+
54+
def percentile(values, rank):
55+
require(bool(values), 'no measured samples')
56+
return sorted(values)[math.ceil(len(values) * rank / 100) - 1]
57+
58+
59+
def statistics(samples, epochs):
60+
duration = sum(epoch['end_ns'] - epoch['start_ns'] for epoch in epochs) / 1e9
61+
require(duration > 0 and math.isfinite(duration), 'invalid measured duration')
62+
latencies = [(sample['end_ns'] - sample['start_ns']) / 1e6 for sample in samples]
63+
return dict(duration_s=duration, throughput_rps=len(samples) / duration,
64+
**{'p%d_ms' % rank: percentile(latencies, rank) for rank in (50, 95, 99)})
65+
66+
67+
def validate_observation(value, run_id, protocol):
68+
require(value['run_id'] == run_id and value['protocol'] == protocol, 'stale run/protocol')
69+
require(value['units'] == UNITS, 'wrong units')
70+
require(value['clock']['name'] == 'monotonic_ns' and positive_integer(value['clock']['resolution_ns']), 'wrong clock')
71+
count = value['requested']
72+
require(positive_integer(count) and count <= 65536, 'request bound')
73+
require(value['attempted'] == count == value['completed'] and value['errors'] == 0, 'incomplete or failed requests')
74+
samples, warmup, epochs = value['samples'], value['warmup'], value['epochs']
75+
require(len(samples) == count and {row['id'] for row in samples} == set(range(count)), 'sample membership')
76+
require(positive_integer(value['warmup_requested']) and value['warmup_requested'] <= 65536, 'warmup bound')
77+
require(value['warmup_requested'] == value['warmup_completed'] == len(warmup), 'incomplete warmup')
78+
require({row['id'] for row in warmup} == set(range(len(warmup))), 'warmup membership')
79+
require(epochs and len(epochs) <= count, 'missing measured epochs')
80+
previous = 0
81+
for epoch in epochs:
82+
require(positive_integer(epoch['start_ns']) and positive_integer(epoch['end_ns']) and
83+
previous < epoch['start_ns'] < epoch['end_ns'], 'invalid epoch clock')
84+
previous = epoch['end_ns']
85+
members_by_epoch = [[] for _ in epochs]
86+
for sample in samples:
87+
require(type(sample['id']) is int, 'invalid request ID')
88+
require(type(sample['epoch']) is int and 0 <= sample['epoch'] < len(epochs), 'unknown sample epoch')
89+
members_by_epoch[sample['epoch']].append(sample)
90+
epoch = epochs[sample['epoch']]
91+
require(positive_integer(sample['start_ns']) and positive_integer(sample['end_ns']) and
92+
epoch['start_ns'] <= sample['start_ns'] < sample['end_ns'] <= epoch['end_ns'], 'invalid request clock')
93+
require(sample['status'] == 200 and sample['valid'] is True and sample['protocol'] == protocol, 'invalid response')
94+
for index, epoch in enumerate(epochs):
95+
members = members_by_epoch[index]
96+
require(members and max(row['end_ns'] for row in members) == epoch['end_ns'], 'epoch completion boundary')
97+
epoch_ends = [epoch['end_ns'] for epoch in epochs]
98+
for row in warmup:
99+
require(positive_integer(row['start_ns']) and positive_integer(row['end_ns']) and row['start_ns'] < row['end_ns'], 'warmup clock')
100+
following = bisect_right(epoch_ends, row['start_ns'])
101+
require(following == len(epochs) or row['end_ns'] <= epochs[following]['start_ns'], 'warmup contamination')
102+
expected = statistics(samples, epochs)
103+
require(set(value['statistics']) == set(expected), 'missing statistics or units')
104+
for key, result in expected.items():
105+
actual = value['statistics'][key]
106+
require(type(actual) in (int, float) and math.isfinite(actual) and actual > 0 and
107+
math.isclose(actual, result, rel_tol=1e-12), 'statistics disagreement: ' + key)
108+
return expected
109+
110+
111+
def artifact(path):
112+
path = Path(path).resolve()
113+
return dict(path=str(path), bytes=path.stat().st_size, sha256=hashlib.sha256(path.read_bytes()).hexdigest())
114+
115+
116+
def verify_artifact(record):
117+
try:
118+
require(artifact(record['path']) == record, 'changed artifact: ' + record['path'])
119+
except OSError as error:
120+
raise ValueError('missing artifact: ' + record['path']) from error
121+
122+
123+
def validate_server(server, observation):
124+
require(server.get('exit') == 0 and 'error' not in server, 'server exit/cleanup failure')
125+
lines = [line[7:] for line in server['stdout'].splitlines() if line.startswith('COUNTS ')]
126+
require(len(lines) == 1, 'missing or duplicate server observation')
127+
counts = parse_json(lines[0])
128+
require(counts == dict(run_id=observation['run_id'], warmup=observation['warmup_completed'],
129+
measured=observation['completed'], errors=0), 'server/client counts differ')
130+
131+
132+
def validate_receipt(receipt, expected_identity, expected_run_id=None):
133+
require(receipt['schema_version'] == 1 and receipt['status'] == 'passed', 'failed or unknown receipt')
134+
require(receipt['identity'] == expected_identity, 'stale source/package/configuration identity')
135+
require('workload' not in receipt or receipt['workload'] == expected_identity.get('workload'), 'stale workload')
136+
require(len(receipt['run_id']) == 32 and all(c in '0123456789abcdef' for c in receipt['run_id']), 'invalid run identifier')
137+
if expected_run_id is not None:
138+
require(receipt['run_id'] == expected_run_id, 'reused run identifier')
139+
validate_cells([tuple(cell[k] for k in ('linkage', 'loop', 'protocol')) for cell in receipt['cells']], receipt['tls'])
140+
require(receipt['artifacts'], 'missing immutable artifacts')
141+
sample_paths = [cell['observation']['path'] for cell in receipt['cells']]
142+
require(len(sample_paths) == len(set(sample_paths)), 'reused samples across cells')
143+
for record in receipt['artifacts']: verify_artifact(record)
144+
for cell in receipt['cells']:
145+
require(cell['client_exit'] == 0 and cell['fresh_execution'] is True, 'missing live client execution')
146+
for key in ('observation', 'server'):
147+
require(cell[key] in receipt['artifacts'], 'unbound observation artifact')
148+
observation = parse_json(Path(cell['observation']['path']).read_text())
149+
workload = expected_identity.get('workload')
150+
if workload is not None:
151+
require(observation['requested'] == workload['requests'], 'incomplete workload')
152+
concurrency = effective_concurrency(cell['protocol'], workload)
153+
require(cell['concurrency'] == concurrency and observation['concurrency'] == concurrency, 'wrong effective concurrency')
154+
expected_epochs, expected_warmup = epoch_workload(cell['protocol'], workload['requests'], concurrency, workload['warmup'])
155+
require(observation['warmup_requested'] == expected_warmup, 'wrong warmup workload')
156+
require(len(observation['epochs']) == expected_epochs, 'wrong epoch count')
157+
validate_observation(observation, receipt['run_id'], cell['protocol'])
158+
validate_server(parse_json(Path(cell['server']['path']).read_text()), observation)
159+
return receipt

‎scripts/lib/v3_package_integration.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@
99
import v3_package_matrix as matrix
1010

1111

12-
def compile_probe(compiler, sources, flags, libraries, executable, command, env, source, build, prefix, private):
12+
def compile_probe(compiler, sources, flags, libraries, executable, command, env, source, build, prefix, private, compile_options=None):
1313
objects, included = [], set()
1414
for index, unit in enumerate(sources):
1515
obj = executable.parent / (executable.stem + '-' + str(index) + '.o')
1616
depfile = obj.with_suffix('.d')
17-
command(shlex.split(compiler) + ['-std=c++20', '-O0', '-g'] + flags +
17+
command(shlex.split(compiler) + ['-std=c++20'] + (['-O0', '-g'] if compile_options is None else list(compile_options)) + flags +
1818
['-MMD', '-MF', str(depfile), '-c', str(unit), '-o', str(obj)], executable.parent, env)
1919
included.update(dependencies(depfile))
2020
objects.append(str(obj))
2121
matrix.check_dependencies(included, prefix, source, build, private)
2222
command(shlex.split(compiler) + objects + libraries + ['-Wl,-Map,' + str(executable) + '.map', '-o', str(executable)],
2323
executable.parent, env)
24-
return {'dependencies': sorted(included), 'sources': {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in sources},
24+
return {'compile_options': ['-O0', '-g'] if compile_options is None else list(compile_options), 'dependencies': sorted(included), 'sources': {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in sources},
2525
'artifact_sha256': hashlib.sha256(executable.read_bytes()).hexdigest(), 'private_test_headers': private,
2626
'dependency_sha256': {path: hashlib.sha256(Path(path).read_bytes()).hexdigest() for path in sorted(included)},
2727
'link_map': str(executable) + '.map'}

0 commit comments

Comments
 (0)