Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
30e9e71
ci: cover all consumer fixtures in style and strict typing gates
codeforester Oct 2, 2026
710208b
ci: keep Markdown examples under the documentation gate
codeforester Oct 2, 2026
8bb0562
fix: preserve consumer logging ownership and routing
codeforester Oct 2, 2026
6556889
perf: reuse logging locks and cache source paths
codeforester Oct 2, 2026
c87eeda
fix: enforce native Windows bundle retention safely
codeforester Oct 2, 2026
f0f9ece
ci: satisfy Windows retention style and typing checks
codeforester Oct 2, 2026
9679928
perf: skip contended retention housekeeping
codeforester Oct 2, 2026
6280ebb
security: bound and validate discovered project configuration
codeforester Oct 2, 2026
7db92d8
test: exercise recursion failures at bounded YAML composition
codeforester Oct 2, 2026
5e84077
fix: capture child and descriptor stdout in JSON envelopes
codeforester Oct 2, 2026
dd21781
ci: reconcile Project Intake through REST with scheduled recovery
codeforester Oct 2, 2026
a166e45
perf: cache second-precision human log timestamps
codeforester Oct 2, 2026
ee489ee
fix: preserve Python newline bytes in descriptor capture
codeforester Oct 2, 2026
baf0ff3
Merge branch 'enhancement/381-20261003-perf-lifecycle-logging-costs-1…
codeforester Oct 2, 2026
c7c21ae
Merge branch 'bug/378-20261003-bug-run-bundle-retention-is-inoperativ…
codeforester Oct 2, 2026
40b1401
Merge branch 'enhancement/386-20261003-perf-retention-serializes-conc…
codeforester Oct 2, 2026
74d2f5c
Merge branch 'security/385-20261003-security-validate-trust-of-ancest…
codeforester Oct 2, 2026
a83fa1f
Merge branch 'bug/379-20261003-bug-json-single-envelope-stdout-contra…
codeforester Oct 2, 2026
d352e43
Merge remote-tracking branch 'origin/main' into ci/393-20261003-ci-ru…
codeforester Oct 2, 2026
6ee5cc9
Merge branch 'ci/393-20261003-ci-ruff-check-and-mypy-do-not-cover-the…
codeforester Oct 2, 2026
f9787b1
Merge branch 'bug/387-20261003-bug-configure-logger-closes-consumer-o…
codeforester Oct 2, 2026
3780821
Merge branch 'enhancement/381-20261003-perf-lifecycle-logging-costs-1…
codeforester Oct 2, 2026
333ac40
Merge branch 'bug/378-20261003-bug-run-bundle-retention-is-inoperativ…
codeforester Oct 2, 2026
4f08c7d
Merge branch 'enhancement/386-20261003-perf-retention-serializes-conc…
codeforester Oct 2, 2026
369f986
Merge branch 'security/385-20261003-security-validate-trust-of-ancest…
codeforester Oct 2, 2026
514949a
Merge branch 'bug/379-20261003-bug-json-single-envelope-stdout-contra…
codeforester Oct 2, 2026
08017ed
ci: gate concurrent retention and logging throughput
codeforester Oct 2, 2026
6e4394f
fix: avoid racing Windows lock-sidecar initialization
codeforester Oct 2, 2026
bd210cd
Merge branch 'enhancement/381-20261003-perf-lifecycle-logging-costs-1…
codeforester Oct 2, 2026
f1bda9a
Merge branch 'bug/378-20261003-bug-run-bundle-retention-is-inoperativ…
codeforester Oct 2, 2026
a1683c4
Merge branch 'enhancement/386-20261003-perf-retention-serializes-conc…
codeforester Oct 2, 2026
5f39a1f
Merge branch 'security/385-20261003-security-validate-trust-of-ancest…
codeforester Oct 2, 2026
7c65de4
Merge branch 'bug/379-20261003-bug-json-single-envelope-stdout-contra…
codeforester Oct 2, 2026
727d575
Merge branch 'ci/311-20261003-v1-0-make-project-intake-resilient-to-g…
codeforester Oct 2, 2026
73770a5
ci: separate sustained persistence cost from hosted filesystem tails
codeforester Oct 2, 2026
bdf26d9
Merge branch 'ci/393-20261003-ci-ruff-check-and-mypy-do-not-cover-the…
codeforester Oct 2, 2026
c3eb5dc
Merge branch 'bug/387-20261003-bug-configure-logger-closes-consumer-o…
codeforester Oct 2, 2026
0597aaa
Merge branch 'enhancement/381-20261003-perf-lifecycle-logging-costs-1…
codeforester Oct 2, 2026
b24372d
Merge branch 'bug/378-20261003-bug-run-bundle-retention-is-inoperativ…
codeforester Oct 2, 2026
d1fd067
Merge branch 'enhancement/386-20261003-perf-retention-serializes-conc…
codeforester Oct 2, 2026
5ba2267
Merge branch 'security/385-20261003-security-validate-trust-of-ancest…
codeforester Oct 2, 2026
77618cb
Merge branch 'bug/379-20261003-bug-json-single-envelope-stdout-contra…
codeforester Oct 2, 2026
268bb7d
Merge branch 'ci/311-20261003-v1-0-make-project-intake-resilient-to-g…
codeforester Oct 2, 2026
8ca0027
docs: regenerate public API signatures for configuration trust options
codeforester Oct 2, 2026
9191a7e
Merge branch 'bug/387-20261003-bug-configure-logger-closes-consumer-o…
codeforester Oct 2, 2026
b7fd5df
Merge branch 'enhancement/381-20261003-perf-lifecycle-logging-costs-1…
codeforester Oct 2, 2026
331e583
Merge branch 'enhancement/386-20261003-perf-retention-serializes-conc…
codeforester Oct 2, 2026
6ba56eb
Merge branch 'security/385-20261003-security-validate-trust-of-ancest…
codeforester Oct 2, 2026
5d8860e
docs: regenerate public API signatures for configuration trust options
codeforester Oct 2, 2026
6e9d231
Merge branch 'bug/378-20261003-bug-run-bundle-retention-is-inoperativ…
codeforester Oct 2, 2026
dcb4dff
Merge branch 'ci/311-20261003-v1-0-make-project-intake-resilient-to-g…
codeforester Oct 2, 2026
682811f
Merge branch 'bug/379-20261003-bug-json-single-envelope-stdout-contra…
codeforester Oct 2, 2026
07d5301
ci: calibrate logging budget from hosted macOS measurements
codeforester Oct 2, 2026
857e52e
Merge main for branch maintenance (#426)
codeforester Oct 4, 2026
b48e444
Merge updated PR #414 for branch maintenance (#426)
codeforester Oct 4, 2026
0441cec
Merge updated PR #419 for branch maintenance (#426)
codeforester Oct 4, 2026
9012457
Merge updated PR #418 for branch maintenance (#426)
codeforester Oct 4, 2026
f5330b2
Merge updated PR #420 for branch maintenance (#426)
codeforester Oct 4, 2026
65a3689
Merge updated PR #421 for branch maintenance (#426)
codeforester Oct 4, 2026
c4af745
Merge updated PR #417 for branch maintenance (#426)
codeforester Oct 4, 2026
66ce268
Merge updated PR #415 for branch maintenance (#426)
codeforester Oct 4, 2026
e82ce1f
Merge updated PR #416 for branch maintenance (#426)
codeforester Oct 4, 2026
2a0a9bb
fix: preserve incomplete JSON stdout captures
codeforester Oct 5, 2026
3d397ae
security: clarify discovered config verification
codeforester Oct 5, 2026
b7f54fe
ci: isolate scheduled Project reconciliation failures
codeforester Oct 5, 2026
8826871
ci: control hosted contention benchmark batches
codeforester Oct 5, 2026
b00c288
fix: defer contended retention index refresh
codeforester Oct 5, 2026
9db3464
fix: preserve logger routing and levels
codeforester Oct 5, 2026
fbcc75c
fix: harden logging caches and sidecar locks
codeforester Oct 5, 2026
841d1e6
fix: handle Windows retention reparse leaves
codeforester Oct 5, 2026
72336ec
ci: expose consumer typing source coverage
codeforester Oct 5, 2026
3b4fc3f
docs: document process-wide JSON capture
codeforester Oct 5, 2026
311c280
test: verify retention converges after contention
codeforester Oct 5, 2026
ae935d1
fix: serialize logger reconfiguration
codeforester Oct 5, 2026
ff3f148
security: make trust errors actionable
codeforester Oct 5, 2026
97135b0
fix: type incomplete capture failures explicitly
codeforester Oct 5, 2026
f15f2c2
style: clean retention convergence test
codeforester Oct 5, 2026
990cb66
style: format typing gate test
codeforester Oct 5, 2026
9ba2a3a
fix: preserve incomplete JSON stdout captures
codeforester Oct 5, 2026
eb3ae08
docs: document process-wide JSON capture
codeforester Oct 5, 2026
73b7eb7
fix: type incomplete capture failures explicitly
codeforester Oct 5, 2026
50b0695
style: format sidecar lock condition
codeforester Oct 5, 2026
c8e829d
Merge remote-tracking branch 'origin/enhancement/381-20261003-perf-li…
codeforester Oct 5, 2026
496decf
style: format retention lock logging
codeforester Oct 5, 2026
0f35aae
Merge remote-tracking branch 'origin/main' into ci/393-20261003-ci-ru…
codeforester Oct 5, 2026
6efcb60
test: cover debug logging with consumer handlers
codeforester Oct 5, 2026
436807b
test: cover concurrent and inherited logging state
codeforester Oct 5, 2026
2a916ea
test: verify Windows retention pinning and reparse cleanup
codeforester Oct 5, 2026
3f58457
Merge remote-tracking branch 'origin/ci/393-20261003-ci-ruff-check-an…
codeforester Oct 5, 2026
92cad10
Merge remote-tracking branch 'origin/bug/387-20261003-bug-configure-l…
codeforester Oct 5, 2026
e15763e
Merge remote-tracking branch 'origin/enhancement/381-20261003-perf-li…
codeforester Oct 5, 2026
2f96072
Merge remote-tracking branch 'origin/bug/378-20261003-bug-run-bundle-…
codeforester Oct 5, 2026
5e8e01f
Merge remote-tracking branch 'origin/enhancement/386-20261003-perf-re…
codeforester Oct 5, 2026
40d9e2a
Merge remote-tracking branch 'origin/security/385-20261003-security-v…
codeforester Oct 5, 2026
52f2e4e
Merge remote-tracking branch 'origin/bug/379-20261003-bug-json-single…
codeforester Oct 5, 2026
54be619
Merge remote-tracking branch 'origin/ci/311-20261003-v1-0-make-projec…
codeforester Oct 5, 2026
7001a5d
fix: calibrate macOS logging benchmark budget
codeforester Oct 5, 2026
51b411d
fix: allow hosted macOS logging tails
codeforester Oct 5, 2026
1cddd44
fix: calibrate Unix logging benchmark budget
codeforester Oct 5, 2026
df6ee90
fix: relax Unix logging benchmark edge
codeforester Oct 5, 2026
258f90b
ci: gate logging against stdlib throughput
codeforester Oct 5, 2026
0418cd4
Merge main into benchmark gate CI
codeforester Oct 5, 2026
b486c93
ci: tolerate isolated Unix persistence tails
codeforester Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 70 additions & 13 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ scheduler outlier block a change.
| Cold no-op invocation, including startup and dispatch | 2,000 ms | 2,000 ms | 4,000 ms | 4,000 ms |
| Base-cli lifecycle increment over Click warm dispatch | 5 ms | 5 ms | 15 ms | 15 ms |
| Warm invocation and non-persistence feature scenarios | 50 ms | 50 ms | 100 ms | 100 ms |
| File-persistence-enabled scenario | 125 ms | 125 ms | 250 ms | 50 ms |
| File-persistence-enabled scenario | 750 ms | 125 ms | 250 ms | 50 ms |

An initial 31-sample local calibration on macOS (Python 3.14.6, Apple Silicon)
measured approximately 101 ms for base-cli cold import, 0.56 ms for warm
Expand All @@ -77,16 +77,18 @@ CI calibration evidence, not adoption claims or release comparisons; review
subsequent retained artifacts before tightening platform budgets.

October 2026 hosted recalibration separates sustained persistence cost from
filesystem tails on Unix/macOS: median must remain at most **50 ms** and p95
at most **125 ms**. The previous 50 ms p95 cap repeatedly rejected otherwise
unchanged runtime code, including the validation-only PR. Observed pairs were
14.66/118.04 ms (Unix median/p95) and 24.93/61.93 and 26.12/87.37 ms (macOS).
Evidence: [Unix run](https://github.com/basefoundry/base-cli/actions/runs/37048785893)
and [macOS validation-only run](https://github.com/basefoundry/base-cli/actions/runs/37052368353).
A sustained slowdown over 50 ms still fails; p95 over 125 ms also fails.
Windows, WSL, parser, import, and non-persistence limits are unchanged.

Each report is versioned as `base-cli.benchmark` schema version 1 and contains
filesystem tails on Unix/macOS: median must remain at most **50 ms**. The Unix
p95 is a 750 ms filesystem-tail sanity ceiling after the retained #422 runner
artifact reached 588 ms p95 with a 15.7 ms median; macOS retains a 125 ms p95
ceiling. Earlier observed pairs were 14.66/118.04 ms (Unix median/p95) and
24.93/61.93 and 26.12/87.37 ms (macOS). Evidence: [Unix calibration run](https://github.com/basefoundry/base-cli/actions/runs/37048785893),
[macOS validation-only run](https://github.com/basefoundry/base-cli/actions/runs/37052368353),
and [#422 Unix tail artifact](https://github.com/basefoundry/base-cli/actions/runs/37338561106).
A sustained slowdown over 50 ms still fails; the p95 ceiling catches materially
larger filesystem failures without turning an isolated scheduler tail into a
merge blocker. Windows, WSL, parser, import, and non-persistence limits are unchanged.

Each report is versioned as `base-cli.benchmark` schema version 2 and contains
the package version, source revision, UTC timestamp, platform profile, Python
version/ABI, OS release, architecture, CPU count, sample count, medians, p95,
maximum, median absolute deviation, and parser/lifecycle comparison values.
Expand Down Expand Up @@ -149,5 +151,60 @@ leases under the lock.
`RetentionPolicy.safe_defaults()` includes `max_total_bytes=512 MiB`, so the
default policy uses the byte-policy recursive-walk bounds described above. A
consumer that needs only count/age retention can explicitly omit the byte cap.
The concurrent benchmark in #391 measures twelve processes against one warmed
cache and gates the p95-to-serial ratio.
The concurrent benchmark in #391 measures up to twelve processes against one
warmed cache. It caps each batch at the runner's available CPU count and uses
additional batches on smaller runners so the p95-to-serial ratio is less
dominated by scheduler oversubscription.

### Benchmark report v2: contention and logging

`base-cli.benchmark` schema version 2 adds `results.base-cli.stress` and
`stress_budgets` with explicit units. Twelve synchronized subprocesses share a
cache warmed past the default 20-bundle cap. Three batches report 36 invocation
samples, serial/concurrent p95 milliseconds, and their p95 ratio; process import
and the start barrier are outside the invocation timer. Logging measures 3,000
INFO records per sample through a real lifecycle, both with and without persistent
files, reporting microseconds/record and records/second. Each run also measures
plain stdlib `FileHandler` logging in the same process and gates the persistent
logging p95 against that baseline, so runner filesystem noise is represented on
both sides of the comparison.

| Profile | Concurrent / serial p95 cap | Persistent log p95 cap (us/record) | Persistent / stdlib p95 ratio cap |
| --- | ---: | ---: | ---: |
| unix | 6 | 100 | 12 |
| macos | 6 | 200 | 12 |
| windows | 10 | 150 | 12 |
| wsl | 10 | 100 | 12 |

These initial hosted caps allow scheduling/filesystem variation while detecting
material regressions. `--check` rejects missing, nonfinite, and over-budget stress
metrics, including a persistent-to-stdlib p95 ratio above 12x. Reports retain the
same CI artifact name and 90-day retention with the v2 schema marker; consumers
must branch on that marker. Local and first hosted measurements are retained with
this PR before further tightening of the caps.

Development-host calibration (macOS, Python 3.14.6, 31 serial/log samples and
36 concurrent samples): concurrent/serial p95 ratio 2.42; ephemeral logging p95
6.20 microseconds/record; persistent logging p95 14.15 microseconds/record.
These are local measurements; hosted per-profile results are retained separately.

The [first hosted stress run](https://github.com/basefoundry/base-cli/actions/runs/37054920383)
recorded the following 31-sample logging and 36-sample concurrency results:

| Profile | Concurrent / serial p95 | Ephemeral log p95 (us/record) | Persistent log p95 (us/record) |
| --- | ---: | ---: | ---: |
| macos (3-core arm64, Python 3.13) | 4.49 | 16.01 | 46.42 |
| windows | 2.99 | 20.14 | 49.03 |
| wsl | 4.25 | 11.44 | 22.90 |

The retained Unix stress artifact measured 55.58 us/record p95. The initial macOS
40 us/record p95 estimate rejected a run whose persistent median
was 25.82 us/record. The first retained macOS stress run reported 46.42 us/record,
but a repeat on the same 3-core arm64 profile reached 129.66 us/record p95 while
the functional and platform validation jobs stayed green. The hosted macOS cap is
therefore 200 us/record: it retains a meaningful guard above the observed runner
tail without turning filesystem scheduling variance into a false merge blocker.
The corresponding `base-cli-benchmark-{profile}-37054920383` and
`base-cli-benchmark-macos-37297556739` artifacts contain machine metadata and all
measured summaries. The Unix calibration is covered by the hosted evidence linked
above.
182 changes: 180 additions & 2 deletions scripts/benchmark_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import datetime as dt
import importlib.util
import json
import logging
import math
import os
import platform
import statistics
Expand Down Expand Up @@ -48,15 +50,18 @@
"wsl": 100.0,
}
PERSISTENCE_ENABLED_P95_BUDGETS_MS = {
"unix": 125.0,
"unix": 750.0,
"macos": 125.0,
"windows": 250.0,
"wsl": 50.0,
}
DEFAULT_ITERATIONS = 31
FRAMEWORKS = ("base-cli", "click", "typer", "cyclopts")
RESULT_SCHEMA = "base-cli.benchmark"
RESULT_SCHEMA_VERSION = 1
RESULT_SCHEMA_VERSION = 2
CONCURRENCY_RATIO_BUDGETS = {"unix": 6.0, "macos": 6.0, "windows": 10.0, "wsl": 10.0}
LOG_P95_BUDGETS_US = {"unix": 100.0, "macos": 200.0, "windows": 150.0, "wsl": 100.0}
LOG_TO_STDLIB_P95_RATIO_BUDGETS = {"unix": 12.0, "macos": 12.0, "windows": 12.0, "wsl": 12.0}


class Summary(TypedDict):
Expand Down Expand Up @@ -128,6 +133,7 @@ def main() -> int:
_measure_production_invocations(args.iterations)
)
results["base-cli"]["features"] = cast(Any, _measure_base_cli_features(args.iterations))
results["base-cli"]["stress"] = _measure_stress(args.iterations)

report = {
"schema": RESULT_SCHEMA,
Expand All @@ -142,10 +148,22 @@ def main() -> int:
"results": results,
"comparisons": _comparisons(results),
"budgets_ms": _budgets_for_platform(BENCHMARK_PLATFORM),
"stress_budgets": {
"concurrent_to_serial_p95_ratio": CONCURRENCY_RATIO_BUDGETS[BENCHMARK_PLATFORM],
"log_p95_us_per_record": LOG_P95_BUDGETS_US[BENCHMARK_PLATFORM],
"persistent_to_stdlib_log_p95_ratio": LOG_TO_STDLIB_P95_RATIO_BUDGETS[BENCHMARK_PLATFORM],
},
}

failures = _check_results(results) if args.check else []
_write_github_summary(report)
if os.environ.get("GITHUB_STEP_SUMMARY"):
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
summary.write(
"\n### Concurrent retention and logging\n\n```json\n"
+ json.dumps(results["base-cli"].get("stress", {}), indent=2)
+ "\n```\n"
)
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
else:
Expand Down Expand Up @@ -331,6 +349,28 @@ def _check_results(results: dict[str, FrameworkMetrics]) -> list[str]:
feature_budget = _feature_budget_for_platform(name, BENCHMARK_PLATFORM)
if p95 is not None and p95 > feature_budget:
failures.append(f"base-cli {name} p95 exceeded {feature_budget:.0f} ms")
stress = base.get("stress", {})
ratio = stress.get("concurrent_to_serial_p95_ratio")
if (
not isinstance(ratio, (int, float))
or not math.isfinite(ratio)
or ratio > CONCURRENCY_RATIO_BUDGETS[BENCHMARK_PLATFORM]
):
failures.append("concurrent-to-serial p95 ratio is missing, invalid, or exceeds budget")
for name in ("logging_persistent_us_per_record", "logging_ephemeral_us_per_record"):
p95 = _metric_p95(stress, name)
if p95 is None or not math.isfinite(p95) or p95 > LOG_P95_BUDGETS_US[BENCHMARK_PLATFORM]:
failures.append(f"{name} p95 is missing, invalid, or exceeds budget")
stdlib_p95 = _metric_p95(stress, "logging_stdlib_us_per_record")
persistent_ratio = stress.get("persistent_to_stdlib_log_p95_ratio")
if stdlib_p95 is None or not math.isfinite(stdlib_p95) or stdlib_p95 <= 0:
failures.append("logging_stdlib_us_per_record p95 is missing, invalid, or non-positive")
if (
not isinstance(persistent_ratio, (int, float))
or not math.isfinite(persistent_ratio)
or persistent_ratio > LOG_TO_STDLIB_P95_RATIO_BUDGETS[BENCHMARK_PLATFORM]
):
failures.append("persistent-to-stdlib logging p95 ratio is missing, invalid, or exceeds budget")
if BENCHMARK_PLATFORM in {"unix", "macos"} and isinstance(features, dict):
persistence = features.get("persistence_enabled_ms", {})
median = persistence.get("median") if isinstance(persistence, dict) else None
Expand Down Expand Up @@ -702,6 +742,144 @@ def log_with_file(ctx: Any) -> None:
return results


def _measure_stress(iterations: int) -> dict[str, Any]:
from contextlib import redirect_stderr

import base_cli
from base_cli.testing import invoke

worker_code = r"""
import os, sys, time
from pathlib import Path
import base_cli
app = base_cli.App(name="benchmark-shared-retention")
@app.command()
def main(ctx):
pass
_ = app.click_command
if sys.argv[1] == "warm":
for _ in range(22):
assert base_cli.run_app(app, []) == 0
raise SystemExit(0)
if sys.argv[1] == "parallel":
print("ready", flush=True)
deadline = time.monotonic() + 30
while not Path(sys.argv[2]).exists():
if time.monotonic() > deadline:
raise RuntimeError("benchmark start barrier timed out")
time.sleep(0.001)
started = time.perf_counter_ns()
status = base_cli.run_app(app, [])
assert status == 0
print((time.perf_counter_ns() - started) / 1_000_000, flush=True)
"""
# Keep the workload large enough to contend, but do not turn a hosted
# runner's scheduler capacity into the benchmark signal. More batches
# preserve the 36-sample contract on smaller runners.
workers = min(12, max(2, os.cpu_count() or 2))
batches = max(3, (36 + workers - 1) // workers)
with tempfile.TemporaryDirectory(prefix="base-cli-stress-") as temporary:
root = Path(temporary)
env = {**os.environ, "BASE_CLI_CACHE_DIR": str(root / "cache")}
command = [sys.executable, "-c", worker_code]
subprocess.run([*command, "warm"], env=env, check=True, capture_output=True, timeout=60)
serial = []
for _ in range(iterations):
result = subprocess.run(
[*command, "serial"], env=env, check=True, capture_output=True, text=True, timeout=30
)
serial.append(float(result.stdout.strip()))
concurrent = []
for batch in range(batches):
barrier = root / f"start-{batch}"
processes = [
subprocess.Popen(
[*command, "parallel", str(barrier)],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
for _ in range(workers)
]
try:
for process in processes:
assert process.stdout is not None
if process.stdout.readline().strip() != "ready":
raise RuntimeError("concurrent benchmark worker failed before barrier")
barrier.touch()
for process in processes:
stdout, stderr = process.communicate(timeout=30)
if process.returncode:
raise RuntimeError(f"concurrent benchmark failed: {stderr}")
concurrent.append(float(stdout.strip()))
finally:
for process in processes:
if process.poll() is None:
process.kill()
process.communicate()
result_metrics: dict[str, Any] = {
"workers": workers,
"batches": batches,
"concurrent_samples": len(concurrent),
"serial_ms": _summary(serial),
"concurrent_ms": _summary(concurrent),
"concurrent_to_serial_p95_ratio": _summary(concurrent)["p95"] / _summary(serial)["p95"],
Comment thread
codeforester marked this conversation as resolved.
"logging_records_per_sample": 3000,
}
stdlib_samples = _measure_stdlib_logging(iterations, root)
result_metrics["logging_stdlib_us_per_record"] = _summary(stdlib_samples)
for persistent, label in ((False, "ephemeral"), (True, "persistent")):
samples = []
app = base_cli.App(name=f"benchmark-log-{label}", log_to_file=persistent)

@app.command()
def log_records(ctx: Any, _samples: list[float] = samples) -> None:
ctx.log.info("warm source cache")
started = time.perf_counter_ns()
for index in range(3000):
ctx.log.info("record %s", index)
_samples.append((time.perf_counter_ns() - started) / 3000 / 1000)

with open(os.devnull, "w", encoding="utf-8") as quiet, redirect_stderr(quiet):
for _ in range(iterations):
result = invoke(app, [], home=root / label)
if result.exit_code:
raise RuntimeError(f"logging benchmark failed: {result.output}")
result_metrics[f"logging_{label}_us_per_record"] = _summary(samples)
result_metrics[f"logging_{label}_records_per_second"] = _summary([1_000_000 / sample for sample in samples])
persistent_p95 = cast(Summary, result_metrics["logging_persistent_us_per_record"])["p95"]
stdlib_p95 = cast(Summary, result_metrics["logging_stdlib_us_per_record"])["p95"]
result_metrics["persistent_to_stdlib_log_p95_ratio"] = persistent_p95 / stdlib_p95
return result_metrics


def _measure_stdlib_logging(iterations: int, root: Path) -> list[float]:
"""Measure plain stdlib file logging in the same process and run."""

log_dir = root / "stdlib"
log_dir.mkdir(parents=True, exist_ok=True)
logger = logging.getLogger(f"base-cli-benchmark-stdlib-{id(root)}")
logger.handlers.clear()
logger.propagate = False
logger.setLevel(logging.INFO)
handler = logging.FileHandler(log_dir / "run.log", encoding="utf-8")
handler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(handler)
samples: list[float] = []
try:
for _ in range(iterations):
started = time.perf_counter_ns()
for index in range(3000):
logger.info("record %s", index)
handler.flush()
samples.append((time.perf_counter_ns() - started) / 3000 / 1000)
finally:
logger.removeHandler(handler)
handler.close()
return samples


def _measure_runner(iterations: int, callback: Callable[[], Any]) -> list[float]:
samples: list[float] = []
for _ in range(iterations):
Expand Down
Loading
Loading