You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Measured on main (uncontended, CI workflow wall-clock): ~254s → ~166s (−33%).
Baseline c2f4b82a = 253s (post-QW1/QW2, pre-S1) → current b07dbcb7 = 166s (S1 + quick-wins on the flattened graph). The S1 PR run measured 169s independently. Under contention the absolute saving is larger (the old graph reached 280–363s; runs are now 166–253s).
coverage (unit/integration/e2e) now nightly + workflow_dispatch only; PR path coverage-free. Branch coverage enabled (Cobertura). NB xdebug branch coverage is ~6× slower → nightly job timeouts raised (#634)
Legend: ✅ done · 🔵 open PR · ⬜ open · ❌ dismissed
Summary
CI wall-clock on a PR is bimodal: ~254s for an isolated push, ~560s when 3–4 pushes land together. The ~200s gap is runner queueing, not runtime — 14–16 jobs per run compete for the (inferred) ~20-job public-repo runner cap. The intrinsic pipeline floor is ~254s.
Measured over CI runs 1330–1358 (2026-07-12..14) from the Actions API; per-run job+step timings retained. Estimates are marked [EST]; a full "not verified" list is at the bottom.
Baseline — uncontended critical path (~254s)
push → queue 3s
→ Setup Dependencies 78s (37s of it = pulling the 872MB :e2e image)
→ wave 2 (slowest gates):
Integration Tests 166s ← gate in 5/10 runs
Lint & Static 148.5s ← 4–23s behind
slowest E2E shard up to 147s ← gate in the other 5/10
Unit 107.5s, Frontend 74s (never gate)
→ CI Success 3s
Runner-time budget ≈ 1627s/run (~27 min); the 10-shard E2E matrix is 65% of it. Every one of 14 jobs pulls the same 872MB image (~8.6 min of pure pull/run per run). Structural facts (verified in ci.yml): no concurrency:, no paths: filters, no actions/cache in any of the 7 workflows.
Quick wins (S effort)
#
Measure
Wall-clock
Runner-time
Risk
✅ QW1
concurrency groups + cancel-in-progress (4 workflows)
455 → ~254s median (−44%)
shrinks bursts
full gain needs main-scope cancel or a merge queue (coarser bisect); PR-scope is risk-free
✅ S1 — flatten the job graph (drop needs: setup, wave-2 self-installs). [ci: flatten the job graph — drop setup, per-job self-install (#624 S1) #630] The biggest uncontended-wall lever: composer install is 13s in-container vs the 78s Setup + handoff paid today → uncontended 258 → ~188s (−70s); measured 253 → 166s on main. Pair with QW2 (offsets the +100s runner-time from 10 shards self-installing). Subsumes QW8.
✅ S2 — persist PHP tool caches (actions/cache: PHPStan/PHPat/Rector/CS-Fixer). [ci: persist PHP static-analysis tool caches across runs (#624 S2) #632] Measured: Lint 145 → 72s warm (−50%), cache-hit verified (7 MB restored). PHPStan tmpDir + Rector cache moved to host-mounted var/cache; the deferred bit was they wrote to the ephemeral container /tmp. Runner-time win — Integration (~146s) stays the gate, so ~0 wall-clock until S5 cuts it.
⬜ S3 — chromium-free :dev image for the 4 non-browser jobs. Stacked with QW5 → ~10–12s pulls [EST].
✅ S4 — reorder Dockerfile stages (dev tools + chromium before COPY . .). [perf(ci): cache dev/e2e Docker tooling below the code copy (#624 S4) #631, CI green] Every commit re-runs a measured 68.3s of apt/pecl/chromium/composer because the dev+e2e stages build after the app-code copy → E2E build 131 → ~30s, Docker Publish 226 → ~120s. 0 CI wall-clock but faster image availability. Verified: a src change leaves the new devtools stage CACHED.
❌ S5 — CI coverage xdebug → pcov. DISMISSED. pcov is line-coverage-only and would foreclose branch/path coverage for a marginal ~18s. Kept xdebug; see S9 instead. Separately enabled branch coverage (branchCoverage=true → Cobertura) — the thing pcov could not do.
⬜ S6 — paratest -p4 with per-process DBs. Runner-time only until S2/QW2; DB-isolation is the real work [EST].
⬜ S7 — run the Unit job natively (setup-php, no Docker). −~65s/run; overlaps S1/S5.
⬜ S8 — skip heavy jobs on docs-only pushes (in-workflow paths-filter). ~455 → ~40s per docs-only push (real here: the two docs(adr-024) commits ran the full matrix). Must be in-workflow so the required CI Success check still resolves.
✅ S9 — coverage off the PR path → nightly. [ci: coverage off the PR path → nightly + branch coverage (#624 S9, drops S5) #633] unit/integration/e2e run coverage-free on PR & push; a nightly schedule + workflow_dispatch re-collect it (with branch coverage, Cobertura) and upload to Codecov. Only CI Success is required, so the per-PR Codecov uploads were safe to drop.
Projected "after" (uncontended wall-clock)
Quick wins only: median ~235s verified (QW1 + QW3 + QW4); ~205s if the QW5 zstd estimate holds. Runner-time 1627 → 1050–1100s (−35%, dominated by QW2).
Everything:~150–190s, anchored at ~188s from S1 alone (measured arithmetic); the ~150s lower bound stacks several [EST] figures and is directional until a trial run. Runner-time → 850–900s (−45%). Docker Publish → ~110–120s.
Actual so far:166s on main with QW1/QW2 + step-2 quick wins + S1 landed — already inside the target band.
✅/⬜ S2 [ci: persist PHP static-analysis tool caches across runs (#624 S2) #632, measured] + QW5 + S3 — the caching/image-diet block. S2 done (Lint −50% warm) but wall-clock-neutral until S5 (pcov) cuts the Integration gate — that is the next real wall-clock lever. QW5/S3 are runner-time [EST].
Not verified (estimates)
zstd pull speedup (QW5/S3); 412MB layer = chromium (S3); PHPStan cache hit rate (S2); pcov overhead (S5); paratest DB isolation (S6); the post-change composition band (computed from per-step medians, not a trial run); docs-only push frequency (S8); the ~20-job runner cap (inferred, not read from billing); xdebug/video overheads (S9/QW9). Full detail with per-measure arithmetic is in the analysis report.
Status — updated 2026-07-15
Measured on main (uncontended, CI workflow wall-clock): ~254s → ~166s (−33%).
Baseline
c2f4b82a= 253s (post-QW1/QW2, pre-S1) → currentb07dbcb7= 166s (S1 + quick-wins on the flattened graph). The S1 PR run measured 169s independently. Under contention the absolute saving is larger (the old graph reached 280–363s; runs are now 166–253s).concurrencygroups + cancelnetworkTimeout$2y$formatvideo: on-first-retrysetup)devtoolsstage caches dev/e2e tooling below the code copy — verified: after a src change the whole stage stays CACHED. 0 PR-wall-clock; Docker Publish ~226→120sisolate: false--no-file-parallelismvs 576 passed reverted (forks pool masked it); gain ~0.3s (noise)tar.zst:e2eimage[EST]— needs a verification pushbakefor Docker Publishtsccache:devimage[EST]S5 coverage xdebug → pcov[EST]workflow_dispatchonly; PR path coverage-free. Branch coverage enabled (Cobertura). NB xdebug branch coverage is ~6× slower → nightly job timeouts raised (#634)Legend: ✅ done · 🔵 open PR · ⬜ open · ❌ dismissed
Summary
CI wall-clock on a PR is bimodal: ~254s for an isolated push, ~560s when 3–4 pushes land together. The ~200s gap is runner queueing, not runtime — 14–16 jobs per run compete for the (inferred) ~20-job public-repo runner cap. The intrinsic pipeline floor is ~254s.
Measured over CI runs 1330–1358 (2026-07-12..14) from the Actions API; per-run job+step timings retained. Estimates are marked [EST]; a full "not verified" list is at the bottom.
Baseline — uncontended critical path (~254s)
Runner-time budget ≈ 1627s/run (~27 min); the 10-shard E2E matrix is 65% of it. Every one of 14 jobs pulls the same 872MB image (~8.6 min of pure pull/run per run). Structural facts (verified in
ci.yml): noconcurrency:, nopaths:filters, noactions/cachein any of the 7 workflows.Quick wins (S effort)
concurrencygroups + cancel-in-progress (4 workflows)networkTimeout(one test hangs 10.7s):e2eimage zstd-compressed [EST]:e2e-zstdpush firstbakefor all 3 Docker Publish targetsisolate: false(564/564 pass locally) — DISMISSED: leaks vi.mock/ESM registry, breaks under--no-file-parallelismtar.zst(keeps.binsymlinks)video: on-first-retrypull_requesttsc+ cached.tsbuildinfoStructural (M effort)
needs: setup, wave-2 self-installs). [ci: flatten the job graph — drop setup, per-job self-install (#624 S1) #630] The biggest uncontended-wall lever: composer install is 13s in-container vs the 78s Setup + handoff paid today → uncontended 258 → ~188s (−70s); measured 253 → 166s on main. Pair with QW2 (offsets the +100s runner-time from 10 shards self-installing). Subsumes QW8.actions/cache: PHPStan/PHPat/Rector/CS-Fixer). [ci: persist PHP static-analysis tool caches across runs (#624 S2) #632] Measured: Lint 145 → 72s warm (−50%), cache-hit verified (7 MB restored). PHPStantmpDir+ Rector cache moved to host-mountedvar/cache; the deferred bit was they wrote to the ephemeral container/tmp. Runner-time win — Integration (~146s) stays the gate, so ~0 wall-clock until S5 cuts it.:devimage for the 4 non-browser jobs. Stacked with QW5 → ~10–12s pulls [EST].COPY . .). [perf(ci): cache dev/e2e Docker tooling below the code copy (#624 S4) #631, CI green] Every commit re-runs a measured 68.3s of apt/pecl/chromium/composer because the dev+e2e stages build after the app-code copy → E2E build 131 → ~30s, Docker Publish 226 → ~120s. 0 CI wall-clock but faster image availability. Verified: a src change leaves the newdevtoolsstage CACHED.branchCoverage=true→ Cobertura) — the thing pcov could not do.paths-filter). ~455 → ~40s per docs-only push (real here: the twodocs(adr-024)commits ran the full matrix). Must be in-workflow so the requiredCI Successcheck still resolves.schedule+workflow_dispatchre-collect it (with branch coverage, Cobertura) and upload to Codecov. OnlyCI Successis required, so the per-PR Codecov uploads were safe to drop.Projected "after" (uncontended wall-clock)
1050–1100s (−35%, dominated by QW2).[EST]figures and is directional until a trial run. Runner-time →850–900s (−45%). Docker Publish → ~110–120s.Recommended order
[EST].Not verified (estimates)
zstd pull speedup (QW5/S3); 412MB layer = chromium (S3); PHPStan cache hit rate (S2); pcov overhead (S5); paratest DB isolation (S6); the post-change composition band (computed from per-step medians, not a trial run); docs-only push frequency (S8); the ~20-job runner cap (inferred, not read from billing); xdebug/video overheads (S9/QW9). Full detail with per-measure arithmetic is in the analysis report.