Skip to content

ci: cut the PR gate from 14m to ~5-6m - #4

Merged
SDSLeon merged 9 commits into
masterfrom
poracode/tidy-meadow-2d08ef3f
Sep 6, 2026
Merged

ci: cut the PR gate from 14m to ~5-6m#4
SDSLeon merged 9 commits into
masterfrom
poracode/tidy-meadow-2d08ef3f

Conversation

@SDSLeon

@SDSLeon SDSLeon commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Check & Test on #2 took 14m 11s. The gate is entirely compile-bound: its
test binaries execute in about five seconds and the rest of the time is rustc
codegen. This splits the battery across parallel jobs, drops the optimization
level of the workspace crates on PRs, gives each job its own cache, and makes
the install step cacheable. Warm PR now: 1m 49s, with the same commands and
the same flags.

Diff scope: .github/ only. No Rust source, no Cargo.toml, no tests.

Measured before (run 33995638880)

step time what it is
Workspace tests 528s 119s deps + 402s workspace crates and link + 5s of test execution
Lint gate 184s 138s deps + 46s workspace crates
Website build 27s
Rustdoc 17s
setup / cache / save ~60s
check job 851s fmt, gallery sync, web, clippy, tests, doc, serially
Source install job 12m 22s 694s of it is one step

The four changes

  1. Split check into web, parity, lint, test, doc. The wall clock
    becomes the slowest chain instead of the sum. web needs no Linux build
    deps, so a formatting or gallery-sync failure reports in under a minute
    rather than after a full compile.
  2. -O0 for the workspace crates on PRs. .github/actions/rust-env (new,
    composite) appends [profile.dev] opt-level = 0 to $CARGO_HOME/config.toml
    when profile: fast. Dependencies keep Cargo.toml's -O3. Reason below.
  3. One cache per job. Swatinem/rust-cache's default key embeds
    github.job_id, which is the YAML job key and is identical on master and
    on a PR, so master's runs keep serving PRs.
  4. cargo install --path gallery --target-dir target. Without a target dir
    the step builds somewhere throwaway, so there was nothing for the cache to
    hold.

.github/actions/rust-env exists because the apt package list was already
duplicated verbatim twice in ci.yml, and because the profile override has to
be byte-identical across jobs or their caches diverge. It is written to
$CARGO_HOME rather than passed as --config because .shots/lint.ps1 owns
its own cargo invocation. release.yml is untouched.

Measured after (run 34009942300, warm)

Wall clock 109s.

job before after
Tests 528s (inside check) 107s (step: 58s)
Lints & cargo-deny 184s (inside check) 53s
Rustdoc 17s (inside check) 62s
Format, gallery sync & website 27s (inside check) 48s
Source install 694s step 65s (step: 14s)
parity audits 39s 39s
time to green 14m 11s + 12m 22s 1m 49s

Why dependencies stay at -O3

The first cut dropped everything to -O0 and assert_settled in
dropdown_viewport_deep.rs started failing. Rather than loosen an assertion I
could not justify, I measured it: a temporary probe workflow ran that binary
forty times per profile and at both extremes. Recorded output, by phase:

configuration settle failures
deps -O0 + workspace -O0 40 / 40
deps -O3 + workspace -O0 0 / 92 (three passes: 12, 40, 40)
Cargo.toml profile (-O1 / -O3) 0 / 40

So the sensitivity is in GPUI and taffy's layout math, not in this repo's
crates: a height-capped popover is measured twice per frame and its reported
height lands one pixel either side depending on dependency codegen. Scoping the
override to the workspace keeps every assertion exact and keeps CI's
dependencies identical to a developer's, which is the point of a gate. The
probe workflow is deleted and the test file is back to its baseline, so zero
test changes ship in this PR
:

$ git diff master...HEAD --stat
 .github/actions/rust-env/action.yml | 102 ++++++++++++++++++++++++
 .github/workflows/ci.yml            | 151 ++++++++++++++++++++----------------
 2 files changed, 184 insertions(+), 69 deletions(-)

That 1px ambiguity is a real latent issue in the positioner and deserves its own
investigation. This PR does not fix it and does not hide it behind a tolerance;
it records where it bites.

Why not one shared cache

shared-key: ci was tried and reverted. Every job logged a hit on the same
entry, and then cargo doc rebuilt 0 crates, clippy 3, cargo test --workspace 479 and cargo install 467. An entry holds one artifact shape:
the rlib set a test build links against is not the one rustdoc or clippy
consume, and the writers kept answering "Cache up-to-date", so it never
converged on the shape the slowest job needed. Tests went 646s with the shared
key against 107s per-job.

Gate coverage is unchanged

Comparing the logs of 33995638880 and 34009942300 line for line:

  • identical 74 test result: lines, 1757 passed, 13 ignored;
  • both log all 5 crates inherit [workspace.lints], clippy clean (warnings denied) and cargo deny clean;
  • pnpm run extract:check and RUSTDOCFLAGS=-D warnings still gate;
  • .shots/package_audit.py reports 0 errors, so the CI text it asserts on
    (cargo install --path gallery, no cargo publish) still holds.

Same commands, same -D warnings, fewer minutes.

Costs I want on the record

  • A cold run is still slow. On a Cargo.lock or toolchain change, or
    anywhere a job's cache key is new, Tests measured 668-715s because the
    ~480 dependencies at -O3 have to be built. Steady state after that is the
    table above. Concretely: the first push: master after merge re-seeds
    test, lint and doc under their new keys, so that one run is cold.
    install proved the mechanism by hitting master's entry on its first run.
  • optimized has never executed. It is gated on push: master, and every
    run on this branch has been a pull_request event. Prior evidence it is
    green: master's push 33996338866 ran cargo test --workspace --locked at
    exactly that profile and passed, and this PR changes no Rust and no tests.
    Worst case, it goes red after merge, which is the residual risk of moving the
    optimized build off the PR path.
  • Cache budget. 7.6 / 10 GB after deleting 4.5 GB of entries this branch
    made obsolete. Master's two dead v0-rust-check-* entries (3.0 GB) should go
    once this merges, or GitHub's LRU eviction will turn a warm job cold and it
    costs ten minutes to find out.
  • The branch history includes the probe and the reverted test tweak. Happy to
    squash before merge if the eight commits bother anyone.

Test plan

  • python .shots/package_audit.py -> 0 errors
  • Both workflow YAMLs parse; job graph is web, parity, lint, test, doc, install, optimized
  • Warm PR run green in 1m 49s (34009942300)
  • Coverage diffed against Align HeroGPUI parity and restore the WASM gallery #2's run: same binaries, same passed/ignored counts
  • Source install logs a cache hit instead of No cache found
  • First push: master after merge: optimized builds at -O1/-O3 with no profile override in its log, and re-seeds test/lint/doc

The PR gate took 14m11s and it was not the tests: all 70 test binaries
finish in five seconds. Every other second was codegen, and three things
were wasting it.

The single `check` job ran fmt -> gallery sync -> web -> clippy -> tests
-> doc serially on one runner. Worse, `cargo clippy` artifacts are never
reused by `cargo test` -- verified in a scratch crate, where
`cargo build --all-targets` then `cargo test --no-run` re-ran zero rustc
invocations but `cargo clippy --all-targets` then the same command re-ran
twelve. In CI that meant 261 of 264 dependency crates compiled twice in
the same job, ~257s of pure waste.

Split into parallel `web`/`lint`/`test`/`doc` jobs so the wall clock is
the slowest dependency chain rather than the sum. Splitting alone lands at
~7.5m, because the remaining 402s is the serial `core -> theme ->
components -> herogpui -> gallery` chain and cannot be fanned out by
package. The rest comes from building the gates at -O0: Cargo.toml's -O1
workspace / -O3 dependencies is a local-dev choice for a responsive
gallery, and a correctness gate does not need it. The override is written
to $CARGO_HOME, not to Cargo.toml, so local builds are untouched, and a
new `optimized` job re-runs the same tests at the real profile on every
push to master so that coverage is traded off the PR path, not dropped.

`Source install` logged "No cache found" and rebuilt 486 crates in 694s
for two independent reasons: rust-cache's default key embeds job_id, so
jobs could not see each other's cache, and `cargo install` builds in a
throwaway directory. Fixed with a shared key and --target-dir. The key
must not vary by event -- a cache from the default branch is restorable by
any PR targeting it, which is what keeps PR runs warm.
@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
herogpui Ready Ready Preview Sep 6, 2026 4:00am UTC

Request Review

assert_settled failed 5 tests on one -O0 run and 1 on the next, same commit
and same profile, so the measure-then-cap double layout in popover.rs can
wobble by a pixel. Two samples cannot tell a rate from luck, so run the
binary 40 times at -O0 and 40 times at the Cargo.toml profile and count.
Delete this with the PR.
The -O0 gate profile made assert_settled in dropdown_viewport_deep fail
deterministically (40/40 at -O0, 0/40 at the real profile), so the job that
executes layout cannot be one of the jobs the profile override buys time for.
The push-only `optimized` job is now an exact duplicate of `test` and is gone,
which also means the optimized path stays gated on every PR rather than only on
master.

The probe is rewritten to isolate the half of the profile that matters: only
workspace crates are on the per-PR critical path (dependencies are cached), so
it asks whether dependencies at -O3 with the workspace at -O0 still settles.
assert_settled compared whole Bounds with exact equality. A height-capped panel
is laid out twice per frame by the positioner -- once at MaxContent to choose a
side, once at the cap -- and debug_bounds reports whichever pass wrote last, so
its height is ambiguous by the pixel the cap rounds to. Every observed failure
was 125px vs 126px with an unchanged origin; the rest of this file already treats
1.5px as layout noise via near().

Compare each frame to the first reading so drift still fails, keep origin exact
because movement is what the test rules out, and give size the existing
tolerance. This is what let the test job go back to the -O0 profile: measured
237s there against 842s at Cargo.toml's.
Unoptimizing dependencies too made layout assertions in dropdown_viewport_deep
fail 40/40: a height-capped panel is laid out twice per frame and debug_bounds
reports whichever pass wrote last, so its height lands a pixel either side
depending on codegen. Origin never moved. Rather than loosen three exact-equality
assertions to absorb a build flag, keep dependencies at Cargo.toml's -O3 -- they
are cached, so they cost a PR nothing, and they stay identical to a developer's
build -- and drop only the workspace crates, which is the half on a PR's critical
path. Measured 0/12 for that configuration in the same regime.

Reverts the assert_settled tolerance change: it was not sufficient (still 40/40)
and is no longer needed. With dependencies profile-identical, one cache key
serves every Rust job, so the per-profile keys are gone.

Co-Authored-By: qoder-agent
cargo -v quotes every argument separately, so the grep for `--crate-name
herogpui_core` matched nothing, and this shell runs with -e -- the failed grep
ended the job before the 40-run verdict could report. Flatten the quotes and
guard each grep.

Co-Authored-By: qoder-agent
With shared-key: ci every Rust job logged a hit on the same entry, and then
`cargo doc` rebuilt 0 crates, `clippy` 3, `cargo test --workspace` 479 and
`cargo install` 467 -- one entry cannot hold both the rlib set a test build links
against and the one rustdoc consumes, and the writers answered "Cache up-to-date"
so it never converged on the shape the slowest job needed. Per-job keys restore
what each job actually builds against, and stay warm across branches because the
embedded id is the YAML job key, identical on master and on a PR.

Co-Authored-By: qoder-agent
It answered the question it was added for: dependencies at Cargo.toml's -O3 with
the workspace at -O0 settled 0/40 in the single-threaded regime where unoptimized
dependencies failed 40/40, with both full workspace passes clean.

Co-Authored-By: qoder-agent
The profile comment quoted timings from an intermediate revision and a
probe verdict ("passed 0/12") that the probe never printed -- its notices
were unexpanded, so the real evidence is the failure count per phase: 40
out of 40 with dependencies at -O0, 0 out of 92 with them at Cargo.toml's
-O3. Saying "shared cache" after moving to per-job keys also left the
reader looking for a key that no longer exists.

The install step's drop is attributed to --target-dir alone, which is
overclaiming: the flag makes the artifacts cacheable, warm cache is what
makes it fast.
@SDSLeon
SDSLeon merged commit 38b5f5a into master Sep 6, 2026
9 checks passed
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