Skip to content

ci: add the performance standard's jobs, off until a caller opts in - #118

Merged
NSchatz merged 2 commits into
mainfrom
feat/bench-jobs
Nov 10, 2026
Merged

NSchatz merged 2 commits into
mainfrom
feat/bench-jobs

Conversation

@NSchatz

@NSchatz NSchatz commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

What

The performance standard's two jobs in the shared CI, both off until a caller opts in.

  • ci.yml takes four new inputs, after every existing one: run-scaling-gate and run-bench-compare (both false), bench-suites ("") and bench-target ("dist/index.mjs"). They add two jobs after mutation, with no needs:, on ubuntu-latest at the first Node line:
    • scaling-gate installs from the lockfile and runs pnpm test:perf, the caller's run of the scaling gate from @cosyte/test-utils/perf. It fails when that fails, so a caller can make it required. Timeout 15 minutes.
    • bench-compare runs on pull requests only. It checks out the whole history, installs and builds the head, adds a worktree at the base commit and installs and builds it there, then runs cosyte-bench compare (from @cosyte/bench) for each suite: every bench/*.bench.mjs and bench/*.bench.ts, or the ones bench-suites names. The comparison interleaves 12 rounds of base and head on one runner. The Markdown goes in the job summary. The results-v1 documents (validated with cosyte-bench check) and the Markdown go in the bench-comparison artifact for 30 days.
    • bench-compare never fails. Everything runs in one step that ends 0. A regression (tool exit 1) becomes a warning annotation, a case it could not measure (exit 2) becomes a notice, and each of these ends the step with a note in the summary: a base that does not build, a missing suite, no suites, and a spent 600 second budget. Timeout 20 minutes, to catch a hung install.
  • self-check.yml: systems turns both jobs on against test/fixtures/ci-caller/. The fixture now installs the real @cosyte/bench 0.1.1 and @cosyte/test-utils 0.1.4. Its test:perf runs the scaling gate and assertScalingGateFires on a synthetic line splitter, and fails on a skip. bench/greeting.bench.mjs is its suite. A new performance-artifacts job (pull requests only, needs: [systems]) requires the kept document to be a results-v1 comparison with at least one case.
  • Docs: README "The performance standard's jobs", and CHANGELOG "Unreleased".
  • Tests: new test/performance-jobs.test.mjs. The job and input lists that other tests pin now include the new ids. Each is an addition, and no existing id was renamed.

Why

The performance standard in cosyte/config (decision 0005) has two instruments:

  • The scaling gate is the required check for complexity regressions.
  • The base against head comparison catches constant-factor regressions. It is advisory until an A/A calibration of a caller's suite on its runner class shows false alarms under 1%.

Section 7 of the decision says the comparison is a reusable job in this repository. Both tools are now on npm (@cosyte/bench 0.1.1, @cosyte/test-utils 0.1.4), so libraries can opt in.

A caller that sets none of the new inputs gets two skipped checks, ci / scaling-gate and ci / bench-compare, and no other change.

Proof

  • Red first: node --test test/performance-jobs.test.mjs against the base failed all 11 of 11 tests. With the change it passes, and the full suite passes 1296 of 1296.
  • harness gate: actionlint 1.7.12 with shellcheck, node --test "test/*.test.mjs", check-no-emdash, package-lists. All passed.
  • The caller run is this PR's own Self-check / systems run, which turns both inputs on against the fixture with the real tools. Measured wall times of the new jobs:
    • systems / scaling-gate: 32s. The gate passed on both axes, and the self-check measured a quadratic split at 15.34, against a ceiling of 8.
    • systems / bench-compare: 35s. The base was built from main in a worktree, and the 12 rounds took about 20s. Result: count the greetings 64 lines, +0.4%, interval -0.9% to +2.4%, no-regression. cosyte-bench check reported "a valid comparison document, schema version 1".
    • performance-artifacts: 3s (no-regression, 12 round(s)).
    • Every check on the run is green (run 42834055124). Both new jobs ran in parallel with verify, and the slowest verify leg took 57s, so the new jobs fit well inside 15 minutes.
  • Locally, an A/A cosyte-bench compare of the fixture suite gave no-regression over 12 rounds. jq read that document with the checks performance-artifacts runs.

Choices

  • Advisory without continue-on-error. The comparison is one script that maps every outcome to an annotation and exits 0. The alternative was a job-level continue-on-error, which still shows a red check. To undo this, split the script into steps.
  • Node line. Both jobs run on the first entry of node-versions, like coverage-ratchet and mutation. The standard's calibration was taken on Node 24, so a caller whose first line is 22 compares on a Node the standard has not calibrated. That is acceptable while the comparison is advisory. A bench-node-version input would undo this.
  • Time budget. The comparisons share a fixed 600 second budget so the job stays inside a 15 minute pull request. This is not an input. Make it one if a caller needs more.
  • Base commit. The base is github.event.pull_request.base.sha, checked out as a worktree under RUNNER_TEMP. The suite file is the head's, and both builds are handed to it, as cosyte-bench compare expects.
  • Real tools in the fixture. The fixture uses the real @cosyte/bench and @cosyte/test-utils, not stand-ins, so systems proves the tools work as well as the wiring. The fixture's test:perf treats a skip as a failure. If a runner ever pushes a phase under MIN_PHASE_MS, the fix is to grow the fixture (count.n, size.size in tool.mjs).
  • Test git identities. The pre-push PHI scan reads t@example.invalid as an email with a non-test domain. So the test repositories in test/ci-systems.test.mjs and test/performance-jobs.test.mjs now use a bare word for user.email.

Kind: code
https://claude.ai/code/session_016orFiSvUThaQbgX8v14PXz

ci.yml takes run-scaling-gate and run-bench-compare, both false by
default, and bench-suites and bench-target for the comparison.

- scaling-gate runs the caller's pnpm test:perf, the scaling gate from
  @cosyte/test-utils/perf, and fails when it fails.
- bench-compare runs on a pull request only. It builds the head and, in
  a worktree, the base, then runs cosyte-bench compare for each suite,
  interleaved on one runner. The Markdown goes in the job summary and
  the results-v1 documents in the bench-comparison artifact. Its one
  step ends 0 whatever happens: a regression is a warning, never a red
  check.

self-check.yml turns both on against the CI caller fixture with the
real tools from npm, and performance-artifacts checks the kept
document.

Claude-Session: https://claude.ai/code/session_016orFiSvUThaQbgX8v14PXz
The pre-push PHI scan reads an email-shaped string as an email with a
non-test domain. git accepts a bare word as user.email, which is all
these throwaway repositories need.

Claude-Session: https://claude.ai/code/session_016orFiSvUThaQbgX8v14PXz
@NSchatz
NSchatz merged commit 0be6757 into main Nov 10, 2026
35 checks passed
@NSchatz
NSchatz deleted the feat/bench-jobs branch November 10, 2026 14:35
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