Skip to content

perf: add a reproducible benchmark pipeline#29

Open
t41372 wants to merge 19 commits into
mainfrom
perf/benchmark-pipeline
Open

perf: add a reproducible benchmark pipeline#29
t41372 wants to merge 19 commits into
mainfrom
perf/benchmark-pipeline

Conversation

@t41372

@t41372 t41372 commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a reproducible benchmark pipeline with suites, datasets, budgets, result artifacts, and comparison tooling
  • add PR, nightly, and A/B comparison workflows with pinned harness dependencies
  • preserve raw statistical evidence and runner-image provenance, and reject incomparable or colliding inputs
  • document the benchmark contract and cover the tooling at the repository's 100% test-coverage floor
  • record the baseline below and tighten the bootstrap ratchet bounds from this PR's own CI artifact

Why

The project needs deterministic performance evidence that can distinguish product regressions from incompatible units, harness drift, and GitHub-hosted runner image changes.

This PR measures only — nothing under src/skit/ is touched, by design: the pipeline has to record the "before" that later optimization PRs will be judged against.

Baseline

Generated by this PR's own benchmark CI job (artifact benchmark-results-pr, run 30145102877) — not a local run.

skit 0.4.1.dev0 · profile pr · 2026-07-25 · Linux 6.17.0-1020-azure · Intel Xeon Platinum 8573C ×4 · 15988 MiB · python 3.13.14 · uv 0.11.32 · textual 8.2.8 · pyperf 2.10.0 · runner ubuntu-24.04 image 20260720.247.2

Metric Median p95 n
startup.version.median_ms 132.742 ms 135.747 22
startup.version.over_python_ms 119.446 ms 1
startup.list_json.median_ms 129.415 ms 132.893 22
scale.list_json.n100.median_ms 179.140 ms 182.172 16
scale.list_json.n1000.median_ms 518.662 ms 528.419 15
scale.list_json.per_entry_us 389.413 µs 1
run_overhead.python.overhead_ms 144.032 ms 1
run_overhead.shell.overhead_ms 201.659 ms 1
tui.first_idle.n100.median_ms 160.867 ms 162.753 5
tui.first_idle.n1000.median_ms 307.098 ms 314.159 5
tui.search.n1000.median_ms 125.871 ms 128.323 5
rss.version.peak_kib 30288 KiB 30400 5
rss.list_json.n1000.peak_kib 69808 KiB 69892 5
imports.version.modules 291 1
imports.list_json.modules 291 1
footprint.wheel_bytes 508392 bytes 1
micro.store.list_entries.n1000.median_us 86451.3 µs 88090.1 20
pipeline.duration_s (harness, not a product metric) 315.366 s 1

No skipped cases. 93 metrics total (headline set above); raw samples ship in results.json.

Budgets — enforced: 4 evaluated, 4 passed. Five target rows are violated, which is the expected state on day one: they are the optimization backlog, not a regression.

Violated target Measured Bound
startup.version.over_python_ms 119.446 ms 75
scale.list_json.per_entry_us 389.413 µs 250
rss.list_json.n1000.peak_kib 69808 KiB 65536
imports.version.has_typer 1 0
imports.version.has_rich 1 0

The two import-census ratchets ship tightened from this artifact (check --propose): 400321 (291 measured + 10% headroom), with context stamped from the run that produced it. No README performance claims and no "lightweight" wording land here — those are a later PR, generated from a results.json, after the budgets are ratified.

Validation

  • uv run ruff format --check
  • uv run ruff check
  • uv run ty check
  • uv run zizmor .github/workflows — no findings
  • uv run pytest --cov -q — 5165 passed, 31 skipped, 100% coverage

Merge

Use Squash and merge after required checks pass. Post-merge one-time setup (create gh-pages, enable Pages, dispatch the nightly once, pick alert thresholds after ~14 points) is the checklist in benchmarks/README.md.

Summary by CodeRabbit

  • New Features
    • Added a reproducible performance benchmarking toolkit with PR, full, nightly, and base/head comparison modes.
    • Introduced deterministic benchmark datasets, multi-suite measurement, headline reporting, and exportable comparison evidence.
    • Added performance budget contracts with enforced vs target tiers and ratcheting support.
  • CI / Automation
    • Added pinned, verified benchmark execution using a local Hyperfine installer, plus PR, nightly history publishing, and on-demand A/B compare workflows.
    • Updated .gitignore to exclude benchmark artifacts.
  • Documentation
    • Expanded benchmark contributor and design docs, including the measurement contract.
  • Tests
    • Added comprehensive end-to-end contract tests for the tooling, budgets, parsing, datasets, and CI/workflow behavior.

t41372 added 13 commits July 23, 2026 23:15
…rovenance, trust-split completion

- js/ts analyzer sources: emit whole function chunks (truncation cut closing
  braces; error-recovery parse trees under-reported analyze times ~5x) + validity
  tests (brace balance, python compile, tree-sitter parses without errors)
- benchmark-compare: results carry each side's git identity (--measured-repo),
  micro degrades per-script under BENCH_COMPARE for harness imports and cold
  probes too, compatibility floor documented (sides >= 725f11d)
- trust split completed: envspec.py (constructed-env contract) + hyperfine
  metric-ID minting + datasets.check_reusable (incl. writing-skit version stamp)
  moved under the coverage floor; suites/ keeps only discovery+spawn
- budgets.toml regenerated canonically via render_budgets (byte round-trip
  test); closure/syscalls target rows scoped profile=full; tui.rss.n1000.peak_kib
  naming aligned with the rss suite's grammar
- stale run.json/results.* unlinked at run start (a crashed run must not leave
  old provenance for summarize to merge with fresh partial data)
- tui: peak-RSS skip is a single pre-spawn decision, parse failures crash; search
  span asserts rows both drop AND survive (never the filter-to-zero degenerate
  path), probe char single-sourced as datasets.SEARCH_PROBE_CHAR
- sync tests: hyperfine pin <-> workflows, BENCH_CI_RUNNER <-> runs-on,
  budgets.toml canonical bytes; compare excludes pipeline.* self-timings; bounds
  render as plain numbers; docs updated to implementation truth
…import surface

- design doc tree/section now name envspec.py as the environment contract's home
  (the round-1 move had updated only half the doc)
- dropped the unimplemented pyperf-compare_to promise; metric-ID grammar named in
  code (pipeline.HEADLINE_METRICS) instead of a hand-copied catalog; compare
  inputs phrasing corrected (checkout ref:, never run: interpolation)
- probe-char fossil test now uses SEARCH_PROBE_CHAR; the n=3 boundary of the
  matching-entry guarantee is pinned by parametrized test
- check_reusable docstring honest about same-version branch switches + README
  note to delete .bench/datasets when switching such branches
- import-surface test: benchmarks/ modules may import only runtime deps + pyperf
  (micro/) + textual (tui probe) + stdlib — a dev-only import would kill every
  A/B run at startup
Correctness (report top-15):
- compare: per-unit noise floors (us floor was 2000µs — swallowed every realistic
  micro regression); sides' profile/platform/python mismatch now renders a loud
  not-directly-comparable warning instead of a clean-looking A/B
- derive(): data-driven DERIVATIONS with strict both-or-neither pairs — a renamed
  producer metric fails loud instead of silently dropping a headline number
- compare mode rides on SuitePlan.compare_mode (build_plan), not ambient
  BENCH_COMPARE env — local  degrades identically to CI
- budgets: max must be finite (inf/nan crashed check/--propose); profiles is now a
  list predicate and footprint.wheel_bytes is scoped [pr, full] (compare-profile
  results no longer render a spurious enforced FAIL)
- results: NaN/Infinity rejected everywhere (json.loads parses bare literals; a NaN
  metric silently passed every bound)
- merge: duplicate suite outputs rejected (raw/duration were silently overwritten);
  execute() asserts output.suite == plan.suite
- startup/imports declare ns=(0,) — their empty-library need is plan-visible, not
  satisfied incidentally by sibling suites
- __main__ boundary catches OSError (missing file printed a raw traceback);
  summarize_dir wraps run.json decode errors in PipelineError
- pyperf/hyperfine parsers raise ParseError (not AttributeError/ValueError) on
  malformed elements; census probe re-raises str SystemExit codes as-is

Structure/cleanup (below the report cap, verified CONFIRMED):
- envspec.build_env: the unverified empty-library special case is gone — every
  dataset root passes the manifest guard, micro render/cold use the real n=0 set
- probe char recorded in the manifest and enforced by check_reusable; the tui suite
  filters with the dataset's own recorded char
- parsers.median/stddev delegate to statistics (p95 stays nearest-rank by design);
  envinfo uses skit.models.now_iso; dist_version's test-only probe seam dropped
- hyperfine install: one composite action (.github/actions/install-hyperfine)
  replaces three drifting workflow copies; sync tests pin version+sha+URL to
  benchmarks/hyperfine.py and require every workflow to use the action
- SuiteOutput.skip_all fills both suite fields from one argument (7 sites);
  generate/generate_runover share a _finalize manifest tail; footprint walks
  site-packages once (skit subtotal in the same pass) and tie-breaks the top-10 by
  name; tests use scoped_skit_dirs instead of five hand-rolled env blocks

Skipped with rationale: default-groups release coupling (deliberate, documented
ty-resolution choice), Manifest.kinds retention (diagnostic; now labeled),
datasets --seed/--state-fraction flags (check_reusable already refuses non-default
reuse), blanket subprocess timeouts (job-level timeout-minutes is the designed
hang bound)
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a complete benchmark evaluation pipeline with deterministic datasets, measurement suites, typed results, performance budgets, A/B comparison reports, CI workflows, documentation, and hermetic contract tests.

Changes

Benchmark pipeline

Layer / File(s) Summary
Results, parsing, and environment contracts
benchmarks/results.py, benchmarks/parsers.py, benchmarks/envinfo.py, benchmarks/envspec.py, benchmarks/hyperfine.py
Defines validated result schemas, tool-output parsers, host metadata collection, isolated environments, and Hyperfine integration.
Deterministic datasets and benchmark subjects
benchmarks/datasets.py, benchmarks/fixtures/*, benchmarks/micro/*
Generates reusable datasets, seeded language sources, and pyperf benchmark subjects with invariant checks.
Pipeline plans, budgets, comparison, and CLI
benchmarks/pipeline.py, benchmarks/budgets.py, benchmarks/budgets.toml, benchmarks/compare.py, benchmarks/__main__.py
Adds profile planning, result merging, derived metrics, budget evaluation and ratcheting, comparison reports, and CLI commands.
Suite orchestration and measurements
benchmarks/suites/*
Runs planned suites for startup, scale, overhead, RSS, imports, microbenchmarks, syscalls, footprint, and TUI behavior.
CI workflows and repository integration
.github/workflows/*, .github/actions/*, pyproject.toml, .gitignore, AGENTS.md, benchmarks/README.md, docs/design/benchmarks.md
Wires benchmark execution into PR, nightly, and manual comparison workflows and documents the pipeline contracts.
Contract and regression tests
tests/test_benchmarks_tooling.py
Covers schemas, budgets, parsers, datasets, environments, CLI behavior, workflow synchronization, source validity, and suite outputs.

Estimated code review effort: 5 (Critical) | ~120 minutes

Poem

I’m a rabbit with benchmarks to run,
Through tidy datasets in the sun.
Budgets leap, reports grow bright,
CI checks the numbers right.
Hop, hop—the timings align!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a reproducible benchmark pipeline.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@t41372
t41372 marked this pull request as ready for review July 24, 2026 07:17
@t41372

t41372 commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmarks/__main__.py`:
- Around line 135-144: Update the exception tuple in the command-line error
handler of benchmarks/__main__.py to include OSError alongside the existing
error types. Preserve the current stderr message format and exit status so
missing or unreadable user-supplied paths produce the same clean benchmarks: …
failure response.

In `@benchmarks/hyperfine.py`:
- Around line 19-24: Update HYPERFINE_SHA256 and the corresponding
install-hyperfine checksum reference to
78c3c13b2909f291319760777591e138a8e32c0d8d575c327429188d67228a2a, keeping the
v1.20.0 asset URL unchanged.

In `@benchmarks/micro/bench_analyzers.py`:
- Around line 21-22: Replace the duplicated _EXT mapping and its language
iteration in the benchmark script with the shared sources.LANGS and
sources.EXTENSIONS registries used by the micro suite. Update filename
generation to derive each language’s expected extension from those registries,
and add a contract test that verifies the generated benchmark filenames match
the shared source definitions.

In `@benchmarks/suites/_env.py`:
- Around line 91-142: Add bounded timeouts to both subprocess.run calls in
run_hyperfine and diagnose_cases, and apply the same timeout policy to
subprocess.run calls in the sibling benchmark suite files. Ensure timeout
failures propagate as clear diagnostics rather than allowing a hung benchmark or
hyperfine process to block indefinitely.

In `@benchmarks/suites/footprint.py`:
- Around line 73-83: Add a short delay between failed uv pip install attempts in
the retry loop, applying it only when another attempt remains and preserving the
existing final RuntimeError behavior. Update the retry logic around the
subprocess.run call and _INSTALL_ATTEMPTS so retries use a brief backoff rather
than running immediately.
- Around line 67-79: Update both subprocess.run calls in _closure— the uv venv
creation and uv pip install steps—to pass cwd=ctx.workdir, ensuring both
commands use the external workdir’s configuration rather than the ambient
process directory.

In `@docs/design/benchmarks.md`:
- Line 56: Label the fenced tree block in the benchmarks documentation with the
text language identifier by changing the opening fence to specify text, while
preserving the existing directory listing content.

In `@pyproject.toml`:
- Around line 117-122: Update pyproject.toml at lines 117-122 to remove
benchmarks/__main__.py from the coverage omit list, and at lines 243-249 extend
mutmut’s source_paths to include benchmarks/ alongside src/skit/; no other
benchmark omit entries require changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 992e3cbf-b948-4759-8128-1a10bc57f1fb

📥 Commits

Reviewing files that changed from the base of the PR and between c4a7dcd and bab9485.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (44)
  • .github/actions/install-hyperfine/action.yml
  • .github/workflows/benchmark-compare.yml
  • .github/workflows/benchmark-nightly.yml
  • .github/workflows/benchmark.yml
  • .gitignore
  • AGENTS.md
  • benchmarks/README.md
  • benchmarks/__init__.py
  • benchmarks/__main__.py
  • benchmarks/budgets.py
  • benchmarks/budgets.toml
  • benchmarks/compare.py
  • benchmarks/datasets.py
  • benchmarks/envinfo.py
  • benchmarks/envspec.py
  • benchmarks/fixtures/__init__.py
  • benchmarks/fixtures/noop.js
  • benchmarks/fixtures/noop.py
  • benchmarks/fixtures/noop.sh
  • benchmarks/fixtures/sources.py
  • benchmarks/hyperfine.py
  • benchmarks/micro/bench_analyzers.py
  • benchmarks/micro/bench_launch.py
  • benchmarks/micro/bench_render.py
  • benchmarks/micro/bench_store.py
  • benchmarks/parsers.py
  • benchmarks/pipeline.py
  • benchmarks/results.py
  • benchmarks/suites/__init__.py
  • benchmarks/suites/_env.py
  • benchmarks/suites/_run.py
  • benchmarks/suites/footprint.py
  • benchmarks/suites/imports.py
  • benchmarks/suites/micro.py
  • benchmarks/suites/rss.py
  • benchmarks/suites/run_overhead.py
  • benchmarks/suites/scale.py
  • benchmarks/suites/startup.py
  • benchmarks/suites/syscalls.py
  • benchmarks/suites/tui.py
  • benchmarks/suites/tui_probe.py
  • docs/design/benchmarks.md
  • pyproject.toml
  • tests/test_benchmarks_tooling.py

Comment thread benchmarks/__main__.py Outdated
Comment thread benchmarks/hyperfine.py
Comment on lines +19 to +24
HYPERFINE_VERSION = "1.20.0"
HYPERFINE_SHA256 = "63ad53934062118f5b0be11785e0bb1603d4b91667d1921f2fd8df9a8712040a"
HYPERFINE_URL = (
"https://github.com/sharkdp/hyperfine/releases/download/"
f"v{HYPERFINE_VERSION}/hyperfine-v{HYPERFINE_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
curl -sL https://github.com/sharkdp/hyperfine/releases/download/v1.20.0/hyperfine-v1.20.0-x86_64-unknown-linux-gnu.tar.gz -o hf.tar.gz
sha256sum hf.tar.gz

Repository: t41372/skit

Length of output: 196


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "file:"
python3 - <<'PY'
from urllib.request import urlopen
url = "https://github.com/sharkdp/hyperfine/releases/download/v1.20.0/hyperfine-v1.20.0-x86_64-unknown-linux-gnu.tar.gz"
print(url)
with urlopen(url, timeout=30) as r:
    data = r.read()
print("http_status", r.status, "content_length", len(data), "content_type", r.headers.get("content-type"))
PY

echo
grep -nE 'HYPERFINE_(VERSION|SHA256|URL)' benchmarks/hyperfine.py || true

Repository: t41372/skit

Length of output: 2862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "grep HYPERFINE constants from benchmarks/hyperfine.py:"
grep -nE 'HYPERFINE_(VERSION|SHA256|URL)' benchmarks/hyperfine.py || true

echo
echo "SHA256 constants in repository:"
grep -RInE 'HYPERFINE_SHA256|63ad53934062118f5b0be11785e0bb1603d4b91667d1921f2fd8df9a8712040a' . || true

Repository: t41372/skit

Length of output: 842


🌐 Web query:

https://github.com/sharkdp/hyperfine/releases/download/v1.20.0/hyperfine-v1.20.0-x86_64-unknown-linux-gnu.tar.gz sha256

💡 Result:

The SHA256 checksum for the file hyperfine-v1.20.0-x86_64-unknown-linux-gnu.tar.gz is 642.5 KB (the download itself) with a corresponding sha256 value of 78c3c13b2909f291319760777591e138a8e32c0d8d575c327429188d67228a2a. Correction: Upon reviewing the official release data [1][2], the SHA256 checksum for the requested file hyperfine-v1.20.0-x86_64-unknown-linux-gnu.tar.gz is 78c3c13b2909f291319760777591e138a8e32c0d8d575c327429188d67228a2a.

Citations:


Correct the hyperfine asset checksum to match v1.20.0.

HYPERFINE_SHA256/install-hyperfine currently use 63ad53934062118f5b0be11785e0bb1603d4b91667d1921f2fd8df9a8712040a, but the published GitHub release asset hyperfine-v1.20.0-x86_64-unknown-linux-gnu.tar.gz has checksum 78c3c13b2909f291319760777591e138a8e32c0d8d575c327429188d67228a2a. Update both references so benchmark installs don’t fail at the install-time integrity gate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/hyperfine.py` around lines 19 - 24, Update HYPERFINE_SHA256 and
the corresponding install-hyperfine checksum reference to
78c3c13b2909f291319760777591e138a8e32c0d8d575c327429188d67228a2a, keeping the
v1.20.0 asset URL unchanged.

Comment thread benchmarks/micro/bench_analyzers.py Outdated
Comment on lines +21 to +22
_LINES = (20, 200, 2000)
_EXT = {"python": "py", "shell": "sh", "js": "js", "ts": "ts"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the shared source registry instead of duplicating _EXT.

benchmarks/suites/micro.py:95-103 materializes files from sources.LANGS and sources.EXTENSIONS, while this script maintains a second mapping. If either registry changes, the orchestrator can mark a language available and launch this script, but this loop may omit it or read the wrong filename, producing incomplete benchmark results without a corresponding skip.

Iterate over the shared language list and extension mapping, and add a contract test covering the generated filenames.

Proposed fix
+from benchmarks.fixtures import sources
+
 _LINES = (20, 200, 2000)
-_EXT = {"python": "py", "shell": "sh", "js": "js", "ts": "ts"}

 def main() -> None:
     sources_dir = Path(os.environ["BENCH_SOURCES_DIR"])
     runner = pyperf.Runner()
-    for lang, ext in _EXT.items():
+    for lang in sources.LANGS:
+        ext = sources.EXTENSIONS[lang]
         spec = spec_for(lang)

Also applies to: 28-34

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/micro/bench_analyzers.py` around lines 21 - 22, Replace the
duplicated _EXT mapping and its language iteration in the benchmark script with
the shared sources.LANGS and sources.EXTENSIONS registries used by the micro
suite. Update filename generation to derive each language’s expected extension
from those registries, and add a contract test that verifies the generated
benchmark filenames match the shared source definitions.

Comment thread benchmarks/suites/_env.py
Comment on lines +91 to +142
def run_hyperfine(
ctx: RunCtx,
cases: list[Case],
*,
warmup: int,
min_runs: int,
env: Mapping[str, str],
export_name: str,
) -> tuple[dict[str, Metric], dict[str, object]]:
"""Spawn hyperfine on `cases`, parse its export, return per-case Metrics keyed
`<case>.median_ms` plus the raw times for the artifact."""
if ctx.hyperfine is None:
raise RuntimeError("hyperfine not found") # callers skip-record before this
export = ctx.workdir / f"{export_name}.json"
argv = build_argv(
cases,
warmup=warmup,
min_runs=min_runs,
export_json=str(export),
hyperfine_bin=ctx.hyperfine,
)
proc = subprocess.run( # noqa: S603 — fixed-shape argv built by hyperfine.build_argv
argv,
cwd=ctx.workdir,
env=dict(env),
check=False,
)
if proc.returncode != 0:
# hyperfine swallows the failing command's stderr; an opaque exit code helps
# nobody. Re-run each case once, captured, and report what actually broke.
raise RuntimeError(
f"hyperfine batch {export_name!r} failed ({proc.returncode}); "
f"single-shot diagnosis: {diagnose_cases(ctx, cases, env)}"
)
times_by_case = parse_export(export.read_text(encoding="utf-8"))
return metrics_from_export(times_by_case), {"times_s": times_by_case}


def diagnose_cases(ctx: RunCtx, cases: list[Case], env: Mapping[str, str]) -> str:
reports = []
for case in cases:
probe = subprocess.run( # noqa: S603 — re-running the exact benchmarked argv
list(case.argv),
cwd=ctx.workdir,
env=dict(env),
capture_output=True,
text=True,
check=False,
)
detail = "" if probe.returncode == 0 else f" stderr: {probe.stderr.strip()[-500:]!r}"
reports.append(f"[{case.name} rc={probe.returncode}{detail}]")
return " ".join(reports)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider adding a timeout to spawned subprocesses.

None of the subprocess.run calls here (or in the sibling suite files: footprint.py, imports.py, micro.py, rss.py, syscalls.py) pass timeout=. A hung benchmarked binary or a stuck hyperfine process would block the whole benchmark run indefinitely instead of failing fast with the exact diagnostic these functions are otherwise designed to produce.

♻️ Example fix
     proc = subprocess.run(  # noqa: S603 — fixed-shape argv built by hyperfine.build_argv
         argv,
         cwd=ctx.workdir,
         env=dict(env),
+        timeout=600,
         check=False,
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def run_hyperfine(
ctx: RunCtx,
cases: list[Case],
*,
warmup: int,
min_runs: int,
env: Mapping[str, str],
export_name: str,
) -> tuple[dict[str, Metric], dict[str, object]]:
"""Spawn hyperfine on `cases`, parse its export, return per-case Metrics keyed
`<case>.median_ms` plus the raw times for the artifact."""
if ctx.hyperfine is None:
raise RuntimeError("hyperfine not found") # callers skip-record before this
export = ctx.workdir / f"{export_name}.json"
argv = build_argv(
cases,
warmup=warmup,
min_runs=min_runs,
export_json=str(export),
hyperfine_bin=ctx.hyperfine,
)
proc = subprocess.run( # noqa: S603 — fixed-shape argv built by hyperfine.build_argv
argv,
cwd=ctx.workdir,
env=dict(env),
check=False,
)
if proc.returncode != 0:
# hyperfine swallows the failing command's stderr; an opaque exit code helps
# nobody. Re-run each case once, captured, and report what actually broke.
raise RuntimeError(
f"hyperfine batch {export_name!r} failed ({proc.returncode}); "
f"single-shot diagnosis: {diagnose_cases(ctx, cases, env)}"
)
times_by_case = parse_export(export.read_text(encoding="utf-8"))
return metrics_from_export(times_by_case), {"times_s": times_by_case}
def diagnose_cases(ctx: RunCtx, cases: list[Case], env: Mapping[str, str]) -> str:
reports = []
for case in cases:
probe = subprocess.run( # noqa: S603 — re-running the exact benchmarked argv
list(case.argv),
cwd=ctx.workdir,
env=dict(env),
capture_output=True,
text=True,
check=False,
)
detail = "" if probe.returncode == 0 else f" stderr: {probe.stderr.strip()[-500:]!r}"
reports.append(f"[{case.name} rc={probe.returncode}{detail}]")
return " ".join(reports)
def run_hyperfine(
ctx: RunCtx,
cases: list[Case],
*,
warmup: int,
min_runs: int,
env: Mapping[str, str],
export_name: str,
) -> tuple[dict[str, Metric], dict[str, object]]:
"""Spawn hyperfine on `cases`, parse its export, return per-case Metrics keyed
`<case>.median_ms` plus the raw times for the artifact."""
if ctx.hyperfine is None:
raise RuntimeError("hyperfine not found") # callers skip-record before this
export = ctx.workdir / f"{export_name}.json"
argv = build_argv(
cases,
warmup=warmup,
min_runs=min_runs,
export_json=str(export),
hyperfine_bin=ctx.hyperfine,
)
proc = subprocess.run( # noqa: S603 — fixed-shape argv built by hyperfine.build_argv
argv,
cwd=ctx.workdir,
env=dict(env),
timeout=600,
check=False,
)
if proc.returncode != 0:
# hyperfine swallows the failing command's stderr; an opaque exit code helps
# nobody. Re-run each case once, captured, and report what actually broke.
raise RuntimeError(
f"hyperfine batch {export_name!r} failed ({proc.returncode}); "
f"single-shot diagnosis: {diagnose_cases(ctx, cases, env)}"
)
times_by_case = parse_export(export.read_text(encoding="utf-8"))
return metrics_from_export(times_by_case), {"times_s": times_by_case}
def diagnose_cases(ctx: RunCtx, cases: list[Case], env: Mapping[str, str]) -> str:
reports = []
for case in cases:
probe = subprocess.run( # noqa: S603 — re-running the exact benchmarked argv
list(case.argv),
cwd=ctx.workdir,
env=dict(env),
capture_output=True,
text=True,
check=False,
)
detail = "" if probe.returncode == 0 else f" stderr: {probe.stderr.strip()[-500:]!r}"
reports.append(f"[{case.name} rc={probe.returncode}{detail}]")
return " ".join(reports)
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 111-116: Command coming from incoming request
Context: subprocess.run( # noqa: S603 — fixed-shape argv built by hyperfine.build_argv
argv,
cwd=ctx.workdir,
env=dict(env),
check=False,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 131-138: Command coming from incoming request
Context: subprocess.run( # noqa: S603 — re-running the exact benchmarked argv
list(case.argv),
cwd=ctx.workdir,
env=dict(env),
capture_output=True,
text=True,
check=False,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/suites/_env.py` around lines 91 - 142, Add bounded timeouts to
both subprocess.run calls in run_hyperfine and diagnose_cases, and apply the
same timeout policy to subprocess.run calls in the sibling benchmark suite
files. Ensure timeout failures propagate as clear diagnostics rather than
allowing a hung benchmark or hyperfine process to block indefinitely.

Comment thread benchmarks/suites/footprint.py
Comment thread benchmarks/suites/footprint.py
Comment thread docs/design/benchmarks.md Outdated
Comment thread pyproject.toml
Comment on lines +117 to +122
omit = [
"benchmarks/suites/*", # subprocess orchestration around hyperfine/strace/uv/probes
"benchmarks/micro/*", # pyperf scripts re-exec'd as worker processes
"benchmarks/__main__.py", # thin argparse shim; all logic lives in covered modules
"benchmarks/fixtures/noop.py", # benchmark subject (a script the benchmarks run), not harness code
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A20 -B2 '^\[tool\.mutmut\]' pyproject.toml
fd -e py . benchmarks | sort

Repository: t41372/skit

Length of output: 2479


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pyproject relevant sections =="
sed -n '100,130p;226,252p' pyproject.toml

echo "== benchmarks/__main__.py =="
sed -n '1,220p' benchmarks/__main__.py

echo "== benchmark tests/imports mentioning tests/test_benchmarks_tooling.py and benchmarks package =="
fd -e py . tests | rg '(^|/)test_benchmarks_tooling\.py$|benchmark|micro|suite' | sort
sed -n '1,220p' tests/test_benchmarks_tooling.py

Repository: t41372/skit

Length of output: 16346


Keep benchmark Python code under the mandatory quality gates.

benchmarks/__main__.py wires dispatch/exit-code logic and is omitted from coverage, while mutmut currently mutates only src/skit/ even though benchmarks/ is copied into workers.

  • pyproject.toml#L117-L122: remove benchmarks/__main__.py from coverage omit so its dispatch paths participate in the 100% coverage floor.
  • pyproject.toml#L228: extend source_paths to include benchmarks/ alongside src/skit/; also_copy alone does not add mutation coverage for Python files.
📍 Affects 1 file
  • pyproject.toml#L117-L122 (this comment)
  • pyproject.toml#L243-L249
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 117 - 122, Update pyproject.toml at lines
117-122 to remove benchmarks/__main__.py from the coverage omit list, and at
lines 243-249 extend mutmut’s source_paths to include benchmarks/ alongside
src/skit/; no other benchmark omit entries require changes.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmarks/envinfo.py`:
- Around line 179-183: Update collect_meta() so invoking uv does not raise
FileNotFoundError when the executable is unavailable: catch the missing-command
condition and record the uv version as "unknown". Preserve the existing version
capture when uv is installed, allowing execute() to produce run metadata and
results while suites continue skipping via ctx.uv being None.

In `@tests/test_benchmarks_tooling.py`:
- Around line 1143-1184: Extend test_run_and_summarize_commands to assert that
fake_execute receives the expected _REPO_ROOT value and the configured budgets
value, alongside the existing profile, out, and measured_root assertions. Use
the actual symbols passed by benchmark_cli.main rather than duplicating or
hardcoding equivalent values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 312d0ef6-9812-4a32-95d7-779174c22573

📥 Commits

Reviewing files that changed from the base of the PR and between bab9485 and 66447b7.

📒 Files selected for processing (14)
  • benchmarks/README.md
  • benchmarks/__main__.py
  • benchmarks/envinfo.py
  • benchmarks/micro/bench_analyzers.py
  • benchmarks/suites/_env.py
  • benchmarks/suites/footprint.py
  • benchmarks/suites/imports.py
  • benchmarks/suites/micro.py
  • benchmarks/suites/rss.py
  • benchmarks/suites/syscalls.py
  • benchmarks/suites/tui.py
  • docs/design/benchmarks.md
  • pyproject.toml
  • tests/test_benchmarks_tooling.py
💤 Files with no reviewable changes (1)
  • pyproject.toml

Comment thread benchmarks/envinfo.py Outdated
Comment thread tests/test_benchmarks_tooling.py
@t41372

t41372 commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
benchmarks/envspec.py (1)

69-74: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate that manifest.json is a regular file.

.exists() also accepts a directory named manifest.json, allowing an invalid dataset root past the fail-fast check.

Proposed fix
-    if not (dataset_root / "manifest.json").exists():
+    if not (dataset_root / "manifest.json").is_file():
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/envspec.py` around lines 69 - 74, Update the manifest validation
in the dataset-root setup to require that dataset_root / "manifest.json" is a
regular file, not merely an existing filesystem entry. Preserve the existing
RuntimeError and fail-fast behavior, and continue calling
skit_dirs(dataset_root) only after this validation passes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@benchmarks/envspec.py`:
- Around line 69-74: Update the manifest validation in the dataset-root setup to
require that dataset_root / "manifest.json" is a regular file, not merely an
existing filesystem entry. Preserve the existing RuntimeError and fail-fast
behavior, and continue calling skit_dirs(dataset_root) only after this
validation passes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 833cfaec-7eab-4606-aea3-17742ea8d3d2

📥 Commits

Reviewing files that changed from the base of the PR and between 66447b7 and fd7f39e.

📒 Files selected for processing (2)
  • benchmarks/envspec.py
  • tests/test_benchmarks_tooling.py

t41372 added 2 commits July 24, 2026 19:53
The pipeline design's implementation order ends by tightening the two
import ratchets from the PR's own CI bench artifact — they shipped as
deliberately generous bootstrap bounds (400) only to keep the interim
runs green. That artifact measures 291 modules on the pinned python, so
`check --propose` moves both to 321 and stamps each row's provenance
from the run that produced it.

Two README lines described only the pre-tightening state: `--propose`
stamps `context` (so it is not hand-written), and the merge checklist's
"tighten the bounds" step now happens pre-merge.
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