core/docs/ci-optimization.md
Shay f93f756e44
Some checks failed
smoke / smoke (-m "not quarantine") (pull_request) Successful in 5m35s
lane-shas / verify pinned lane SHAs (pull_request) Failing after 28m37s
ci: order-of-magnitude CI optimization for single 2-vCPU Act runner
Root cause of PR checks stuck "Waiting to run": full-pytest on main ran
~12k tests including the slow registry on a 1.5-CPU / 1.2 GiB job cgroup
for hours, monopolizing the only runner.

Workflows:
- full-pytest.yml: fast lane only (`not quarantine and not slow`)
- nightly-full-pytest.yml: full suite including slow (schedule + manual)
- lane-shas.yml: PR path filters so pin verification is skipped when
  the change cannot affect lane report bytes

Docs: docs/ci-optimization.md + testing-lanes CI mapping.

Infra (VM, same change window): job cgroup 2 CPU / 1800 MiB; hung
full-pytest task stopped so the queue can drain PR smoke/lane-shas.
2026-07-14 20:40:52 -07:00

3 KiB
Raw Blame History

CI optimization (single self-hosted Act runner)

Problem (measured 2026-07-15)

Fact Value
Host 2 vCPU, ~3.8GiB RAM, shared Forgejo + Postgres + Traefik
Job cgroup (was) --cpus=1.5 --memory=1200m
Concurrent capacity 1 job
Suite size ~12.7k tests (not quarantine)
Post-merge job full-pytest with slow included + -n 2
Observed One full-pytest ran 2+ hours at ~25% progress; PR smoke / lane-shas stayed Waiting to run

That is not “slow tests” alone — it is queue starvation: one oversized job owns the only runner.

Order-of-magnitude strategy

1. Right-size the default CI lane (workflow) — largest lever

Lane When Marker expression Intent
smoke every PR fixed file list minutes; merge gate
lane-shas PR (path-filtered) + main pin script only when pin surface can change
full-pytest (main) every push to main not quarantine and not slow fast post-merge ratchet
nightly-full schedule + manual not quarantine (includes slow) soak / proof / eval-matrix

Local doc already measured ~7.7× from “full serial” → “fast + parallel” on a laptop (docs/testing-lanes.md). On a 2vCPU VM the absolute times are worse, but excluding slow removes the 16minute parallel floor and hundreds of soak tests — typically multi× wall clock and frees the runner for PR gates.

2. Path-filter expensive PR jobs

lane-shas is multiminute (demos + showcase). Skip on PR when the diff cannot change pin bytes (docs-only, pure tooling, etc.). Main still always verifies after merge.

3. Runner cgroup (infra)

infra/runner/config.yaml job options should match host reality:

  • Prefer 2 CPUs for the job when the host is 2vCPU (avoid 1.5 + xdist thrash).
  • Memory ≤ available after Forgejo (~800MiB limit on host stack).
  • Keep capacity 1 until a second runner exists — concurrency >1 on 2 cores makes two jobs slower than one.

4. What will not give 100× on this box

  • Throwing more xdist workers at a 2vCPU host.
  • Running full+slow on every main push.
  • Expecting MLX/Rust to speed pytest collection/runtime of the whole suite without suite redesign.

True second order of magnitude needs either more hardware (second runner / larger VM) or further suite partitioning (shard by package, change-based test selection).

Local policy (solo branch work)

  1. Targeted tests for files you touched.
  2. Dual-run only if pin surface changed.
  3. Full smoke before merge (or trust green smoke if unchanged smoke paths).
  4. Never rely on remote full-pytest for PR feedback — it is post-merge / nightly.
  • docs/testing-lanes.md — fast / slow / full classification
  • conftest.pySLOW_FILES / SLOW_TESTS / QUARANTINE
  • .github/workflows/full-pytest.yml
  • .github/workflows/nightly-full-pytest.yml
  • .github/workflows/smoke.yml
  • .github/workflows/lane-shas.yml