Repository navigation
feat(repo)!: time every PR's mutation phases against its base - #274
Merged
Merged
Conversation
Records what origin/main already does for Stream G with citations, the infection losslessness argument, the switch-point contract for Stream F, the per-mutator purity draft, and the implementation plan for PR 1 (report-only same-job A/B bench lane plus a check phase duration). Per REPO-D2 this is the PR's single plan file.
N = 4 in order A B B A B A A B with a non-overlap + 3% signal rule (2/70 null rate per cell), workload digest from mutate files, covering tests from each run's own incremental report and loaded configs/manifests/catalogs, side A = merge-base with the PR's base branch, one build mechanism for both sides, 20-minute target, no artifact upload, incomparable state dropped.
…hase durations Adds .github/workflows/bench.yml (approved under CONST-E9 by supervisor ruling 1, 2026-10-09): builds the PR's merge-base with its base branch and the PR head, runs both A B B A B A A B over the repo slice and the enterprise fixture with --full, and publishes one versioned bench-report.json from which the step summary table and a single ::notice/::error line derive. The verdict line's phaseDurations gains check (union of checker-busy intervals; measured | not-run | not-recorded) and reporting (measured | not-recorded), and RunPhase gains reporting. Pure bench decisions live in test/e2e-core; the orchestrator is the new private test/bench package. The catalog helpers move from test/e2e into test/e2e-core (DEL1: no copy left).
Share SetupStep, BenchCorpusName and the BenchPhase schema instead of redeclaring them; add BenchRunKey.label; reuse the reporting cells for the suppress rule; dedupe covering ids before lookup; name the render property tests after their workflows so test-discipline accepts them.
# Conflicts: # packages/stryker-js/src/Checker/checker-pool.handle.ts
# Conflicts: # packages/stryker-js-cli-contract/tests/run-event-wire-line.integration.test.ts
Every setup or orchestration failure now writes an aborted report with a code and next action, emits ::error and exits non-zero. Each spawned CLI run has its own timeout and a timed-out run is an invalid sample. Failed runs carry their exit code and a bounded stderr tail; an uncomputable workload digest is a distinct unverified state that never reports a speed signal. The phase-split and rendering decisions move into *.workflow.ts modules (ADR-0002), the per-phase reading is one rule table, a measured side cell stores its samples so its statistics are derived, and CheckerBusyIntervalSchema refuses endMs < startMs with one clamp at the clock. The contract-compat check reports a removed constrained union branch. The bench shares the e2e-core lint config and one manifest resolve helper.
The effect tsgo lint (newSchemaClass) fails CI's check on the three `new` constructions moved into catalog-resolution.ts.
Per the sub-conductor CI budget ruling: one bench job per corpus entry, emitted by a plan job from test/bench/corpus.json, each with timeout-minutes 9; a job deadline (BENCH_DEADLINE_MS) clips every run and aborts the report with code budget-exceeded instead of a silent kill. Repo entries pin a mutate range and their dedicated testFiles, identical on both sides. Side B's tarballs come from the released-tarballs cache CI already fills, side A copies them when the pins match. Docs-only PRs skip the lane.
# Conflicts: # packages/stryker-js/src/run/deferrable-dry-run.cell.ts # packages/stryker-js/src/run/mutant-settlement.ts
The report keeps only per-run wall time and counts; the enterprise fixture's executed-test counts swing between runs of one workload, and diagnosing that needs every run's own NDJSON and incremental report.
… the temp outDir The TCE temp program extends the project's tsconfig, so paths aliases pull real sources in and tsc wrote their .js beside them. Later runs resolved those files, changing which tests Vitest selected: the bench's enterprise fixture swung 47-481 executed tests with no edit. bench: restore each side's project tree before every run (main's released checker still leaks) and pin the enterprise mutate set to four files.
…ecision The plan was started, so KTD4b lands in a new dated plan that supersedes it instead of an in-place rewrite; one plan file stays in the PR.
…cked projects The checker's TCE temp program extends the project tsconfig, so tsc wrote pulled-in sources' .js beside them and later runs selected fewer tests. The plan is superseded to correct KTD4b: side A builds main's checker from the merge-base, not a released one.
…-out The enterprise job on d929a6b hung in npm install until the 9-minute job ceiling cancelled it, with no report: the deadline covered only the runs. Setup now runs under the same deadline, setup commands force-kill 10 s after interruption like runs do, and a stall aborts with its own code and next action.
The enterprise job's setup stalled past the deadline on a7ad3b6 and the log named no step. Each timed setup step now logs when it starts and ends.
The enterprise install stalled at the deadline on two heads and its buffered output was discarded on interruption. An interrupted setup command now prints its output tail, and the enterprise npm installs log each registry request, so the tail names the request that stalled.
…commit @effect/opentelemetry and @effect/platform-node 4.0.3 were published with a peer on effect ^4.0.3, which is not published; npm's resolver looped on that peer until the job deadline. Both enterprise npm installs now pass --before the time of the side's last pnpm-lock.yaml commit, so only releases that lockfile could have seen are admitted and both sides with the same lockfile resolve the same versions.
…the report The report said an entry's workload was same or changed, but kept no digest to check that against. Each measured run now carries its executed-test count and workload digest, and the report schema moves to 1.1. The plan's KTD11 is updated in a new dated plan that supersedes the 1050 one.
ryanleecode
approved these changes
Oct 10, 2026
ryanleecode
left a comment
Contributor
There was a problem hiding this comment.
Root verified: all checks green, 0 threads, no suppressions. Over-10m legs inherited (Stream K).
systemfsoftware-maker
added a commit
that referenced
this pull request
Oct 10, 2026
…ersion #282 requires #282 made mainVersion a required field of the version-law input; #274, merged after it, added the nullable-struct and constrained-union scenarios without it, so the package's tests no longer compiled (TS2741) and those cases read as stale-baseline. Both scenarios now declare mainVersion equal to their releasedVersion, as every other 0.4.0 scenario does.
This was referenced Oct 10, 2026
ryanleecode
pushed a commit
that referenced
this pull request
Oct 10, 2026
) * fix(stryker-js): read the ignore reason's code in count-runs #276 read the decoded IgnoreStatusReason's ruleId while #283 renamed that field to code; each PR compiled alone and main 251a511 fails tsc (TS2339). A property law now encodes a drawn IgnoreStatusReason through the schema and requires countRuns to count exactly that code once. * fix(stryker-js-cli-contract): give #274's two law scenarios the mainVersion #282 requires #282 made mainVersion a required field of the version-law input; #274, merged after it, added the nullable-struct and constrained-union scenarios without it, so the package's tests no longer compiled (TS2741) and those cases read as stale-baseline. Both scenarios now declare mainVersion equal to their releasedVersion, as every other 0.4.0 scenario does.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every pull request that changes more than documentation now gets a
Benchworkflow that times a real mutation run on the PR head against the same run on its merge-base, per project and per phase, each project on its own runner. Before this there was no CI evidence of whether a change made a mutation run faster or slower, or which phase the time goes to.First PR of Stream G (execution engine). The later optimisations in the stream (per-test selection, infection filtering, kill-likelihood ordering, warm module swap) each cite this lane's table as their speed evidence; the instrument share also decides whether Stream F's native schemata emitter goes ahead.
What the lane measures
.github/workflows/bench.ymlhas aplanjob that readstest/bench/corpus.jsonand starts onebenchjob per entry. Each job builds side A (the merge-base with the base branch) and side B (the PR head) on its runner, with each side on its own released-tarball pin, and runs both interleaved in the order A B B A B A A B with--fullover that entry:test/bench/corpus.json, one dogfood file each from three packages, chosen so that every phase does real work and one entry has no checker. Each entry is bounded so its eight runs fit the job budget: the vitest-runner entry mutates lines 85-160 of its file, and the vitest-runner and TypeScript-checker entries run only their workflow's property test (testFiles). Both sides get the same bound, and it shapes timing only, never a verdict;PRs that change only
**/*.mdordocs/**skip the workflow. A full-corpus bench belongs onmainand is not part of this lane.Time budget
No bench job may run over 10 minutes, and the whole workflow must finish within 10. Each
benchjob hastimeout-minutes: 9andplanhas 1. Each job also setsBENCH_DEADLINE_MS7.5 minutes after it starts. The orchestrator clips each run's timeout to that deadline, and when the deadline stops a run or arrives before one starts, it writes an aborted report with codebudget-exceeded, the finished runs' times and the next action, emits::errorand exits 1. An overrun therefore fails with a reason before the job ceiling is reached.Measured on head 714dae7, run 38037113409 (whole workflow 6 min 18 s, created to last job done):
The released tarballs come from the
released-tarballscache thatci.ymlalready fills, so neither side runs Nix when the two pins match (side A copies side B's tarballs). Before this change one job ran every entry and took 26 minutes (3.3 minutes of tarball builds, 22.5 minutes of runs).Every timing comes from the run's own NDJSON verdict line, not from wall-clock wrapped around the CLI. The job summary shows one table per project:
A cell only counts as a signal when the four samples on each side do not overlap at all and |Δ median| is at least 3% of A. Under the null hypothesis a cell is a false signal in at most 2/70 ≈ 2.9% of orderings, so treat a lone signal as a reason to re-run, not as proof. Each project also reports its workload status (
same,changedwith the differing entries, orunverifiedwhen a digest could not be computed) and both sides' mutant and executed-test counts. A cell with achangedorunverifiedworkload never signals.Until this PR is on main, side A's verdict lines carry no
checkorreportingduration. Those cells read "not measured", and so doesmutation-teston side A, because the older engine counts reporting time inside it.Contract change (breaking)
phaseDurationsgainscheck(measured {ms}|not-run|not-recorded) andreporting(measured {ms}|not-recorded).checkis checker-busy wall-clock, the union of the intervals in which any checker was starting or checking. It overlaps the other phases.RunPhasegainsreporting, andmutation-testnow stops wherereportingstarts, so the five sequential phases add up to the elapsed time. Exhaustive matches overRunPhasehave to handle the new value.{ "_tag": "not-recorded" }. The new keys ship in streamschemaVersion8.0, the version main's unreleased subsumption change already introduced. The changeset bumpsstryker-js-cli-contractminor andstryker-jsmajor.How an agent consumes the result
bench-report-<entry slug>artifacts from theBenchworkflow run on the PR head, one per corpus entry. Each holds that entry'sbench-report.json.BenchReportJsonfrom@systemfsoftware/stryker-e2e-core(test/e2e-core/src/bench-report.schema.ts,schemaVersion: '1.0').outcomeis a tagged union:summarized:projects[], each withcorpus,entry,workload,counts, androws[]of{ phase, a, b, verdict }. A measured side cell stores every sample (ms,shareOfTotal) in position order, and median, min and max are derived from those samples.verdictisimproved | regressed | no-signal | not-measured | workload-unverified, withdeltaMsanddeltaShareOfAwhere measured.failed:invalid[], each a run key pluscode(stream-undecodable | stream-invalid | timeout),exitCodeand a boundedstderrTail.aborted:code,reasonandnextActionfor any setup or orchestration failure, includingentry-unknownandbudget-exceeded.runs[]holds every run's outer wall-clock and exit code, andsetupSteps[]the time of each setup step.::error title=Bench regression::…when any cell regressed,::errorfor afailedorabortedreport (the job also exits non-zero), and::noticeotherwise. The markdown table and the annotation are both rendered from the decoded report, so they cannot disagree with the artifact.The lane is report-only. It is not part of
ci.yml, so no required status context changes.Code the new tests target in main's mutation report
The dogfood mutation lane mutates
src/**/*.workflow.tsandsrc/**/*.schema.tsin each package. The new laws and integration scenarios target:test/e2e-core/src/summarize-bench.workflow.ts: per-phase rule table, sample separation, the 3% floor, workload same / changed / unverified, and suppression ofmutation-testwhen side A's reporting was not recorded.test/e2e-core/src/read-bench-run.workflow.ts: exactly one verdict line, undecodable or invalid streams, and timeouts with their exit code and stderr tail.test/e2e-core/src/render-bench-report.workflow.ts: table, notice or error choice, failed and aborted markdown, and annotation escaping.test/e2e-core/src/bench-summary.schema.ts,bench-report.schema.ts,bench-run.schema.ts: round-trips and abort codes with their next actions.packages/stryker-js/src/phase-durations.workflow.ts: the five sequential phases sum to the elapsed time, a missing mark yieldsPhaseMarksMissing, andcheckisnot-runwithout a checker.packages/stryker-js/src/phase-durations.schema.ts:CheckerBusyIntervalSchemarefusesendMs < startMs(one clamp, atPhaseClock.recordCheckerBusy), and the checker-busy union is bounded, order-free, split- and duplicate-invariant.packages/stryker-js-cli-contract/src/run-event.schema.ts, through the wire-line and contract-version suites: absent keys decode asnot-recorded, and the contract-compat check now reports a removed constrained union branch.Verification
Run locally on the head:
pnpm typecheck(58/58 tasks), plus the changed and added suites intest/e2e-core(bench-decisions, read-bench-run and render-bench-report),packages/stryker-js(phase-durations, check-cost-record, readmit-subsumed, run-event-stream: 35) andpackages/stryker-js-cli-contract(all 160), all passing. The bench smoke covered theenvironment-incomplete,corpus-unreadable,side-setup-failed,entry-unknownandbudget-exceededaborts: each wrote the aborted report, emitted::errorand exited 1, and no Stryker run started. No bench run or mutation run was done locally. The lane itself runs only in CI, on this PR.Plan:
docs/plans/2026-10-10-0536-perf-execution-engine-bench-lane-plan.md.