Skip to content

ci: complete comparative benchmark contract for #309 - #373

Open
codeforester wants to merge 2 commits into
ci/312-20260918-deduplicate-invariant-gatesfrom
ci/309-20260918-comparative-benchmark-contract
Open

codeforester wants to merge 2 commits into
ci/312-20260918-deduplicate-invariant-gatesfrom
ci/309-20260918-comparative-benchmark-contract

Conversation

@codeforester

Copy link
Copy Markdown
Contributor

Closes #309. This PR is stacked on #372 (ci/312-20260918-deduplicate-invariant-gates); after #372 lands, retarget this PR to main for final review.\n\n### Changes\n- Measures fresh-process import, cold invocation, warm parser dispatch, and base-cli lifecycle/production paths for Click, Typer, Cyclopts, and base-cli.\n- Adds base-cli JSON success/error, diagnostics, nested command, and persistence enabled/disabled scenarios.\n- Makes missing comparators or scenarios fail; enforces per-profile p95 budgets and lifecycle delta over Click.\n- Publishes versioned JSON with source/environment/framework versions, p95/median/MAD, and lifecycle comparisons.\n- Adds public per-platform Actions summaries and retains dated JSON artifacts for 90 days, including WSL.\n- Documents scenario semantics, local commands, thresholds, and calibration evidence.\n\n### Validation\n- Full local validation: 534 tests and 193 subtests passed; 87.20% coverage; typing, lint, contracts, benchmark, and security gates passed.\n- Comparative benchmark passed with 31 samples per scenario on local macOS for all four frameworks.\n- Docs, changelog, YAML, Ruff, and benchmark self-tests passed.

f"base-cli cold invocation p95 exceeded {COLD_INVOCATION_P95_BUDGETS_MS[BENCHMARK_PLATFORM]:.0f} ms"
)

for framework in FRAMEWORKS:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: _check_results gates ALL comparator frameworks' warm_invocation_ms p95 against WARM_INVOCATION_P95_BUDGETS_MS, not just base-cli's.

for framework in FRAMEWORKS:
    warm_p95 = _metric_p95(results.get(framework, {}), "warm_invocation_ms")
    if warm_p95 is not None and warm_p95 > WARM_INVOCATION_P95_BUDGETS_MS[BENCHMARK_PLATFORM]:
        failures.append(...)

Before this PR, only base-cli's own import/invocation metrics were checked against a budget; Click/Typer/Cyclopts were purely informational comparators. This loop now fails --check if any of those external, unpinned-minor libraries has a warm no-op p95 above 50ms/100ms — e.g. a slightly slower point release, or ordinary CI-runner noise (this same PR's docs cite a 26ms MAD on Windows for the persistence scenario, so this class of variance is real on these runners). That turns base-cli's CI red for a reason completely unrelated to any base-cli code change.

Suggest restricting this loop to framework == "base-cli" (matching how cold_import_ms/cold_invocation_ms are already scoped to base-cli only a few lines above), or dropping the loop and relying on production_warm_invocation_ms/feature-scenario checks, which already cover base-cli's own warm-path budget.


command = cast(Any, app.click_command)
runner = CliRunner()
return _measure_runner(iterations, lambda: runner.invoke(command, []).exit_code)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness (measurement isolation): _measure_lifecycle_invocations does not isolate a temp HOME/XDG dir the way _measure_production_invocations and _measure_base_cli_features do (both wrap invocations in tempfile.TemporaryDirectory() and pass home=). Here the app is invoked directly through a bare CliRunner().invoke(command, []) with the ambient process environment and CWD.

This metric feeds the tightest gate in the whole contract: LIFECYCLE_OVERHEAD_P95_BUDGETS_MS (5ms on unix/macos, 15ms on windows/wsl) via lifecycle_increment_over_click_warm_p95_ms. If base-cli's per-invocation path does any HOME/CWD-dependent work (config/history discovery, XDG lookups) even with log_to_file=False, running it against the real ambient environment (a developer's real $HOME, or the CI checkout's CWD) rather than an isolated empty temp dir can make this sample slower/noisier than in a clean environment, tripping the 5ms budget for reasons unrelated to an actual regression, and making the number non-reproducible across machines.

Worth isolating HOME here the same way the other scenarios do, unless the intent is specifically to mirror Click/Typer's own un-isolated CliRunner usage for a fair 'lifecycle overhead over Click' comparison — if so, that tradeoff is worth a one-line comment since it's inconsistent with the isolation used everywhere else in this file.

package_root = Path(__file__).resolve().parents[1] / "lib" / "python"
environment = dict(os.environ)
existing_path = environment.get("PYTHONPATH")
environment["PYTHONPATH"] = f"{package_root}{os.pathsep}{existing_path}" if existing_path else str(package_root)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup (reuse): The PYTHONPATH-environment-setup block is duplicated verbatim between _measure_import (a few lines above) and _measure_cold_invocations (here):

package_root = Path(__file__).resolve().parents[1] / "lib" / "python"
environment = dict(os.environ)
existing_path = environment.get("PYTHONPATH")
environment["PYTHONPATH"] = f"{package_root}{os.pathsep}{existing_path}" if existing_path else str(package_root)

Worth extracting into a small _subprocess_environment() -> dict[str, str] helper both call. As written, a future change to how the source checkout is exposed to the spawned subprocess (e.g. an extra path segment, a different env var) has to be kept in sync by hand across both copies, and a partial edit would silently desync cold-import vs cold-invocation measurement environments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant