Skip to content

CI speed: make the compiler cache store entries and shorten the macOS path #310

Open

Description

@irzhywau

Part of #194. A CI review on 9 October (develop at 8941d478) found new evidence about the compiler cache and seven speed paths that #194 does not yet record. #194 keeps ownership of everything it already lists. This issue adds only the new items, so the lane stays in one place.

Owner: not assigned (Anders triages). Task base and PR target: develop.

Current state: #311 owns the private compiler-cache startup repair and its review. Trusted remote write, warm read and cache-budget proof remain open here. Public PR #326 passed all 11 jobs on its first attempt in run 37973992785 (71m01s overall; macOS 65m23s). This remains a long-run baseline; the private cache repair has not been integrated and hosted time savings are still unproved. PR #326 owns local warning/scope consistency and the installed journey preparation window. The remaining speed items are measured follow-ups after the cache baseline; hosted documentation skips also depend on #317 input coverage.

Summary

A CI run takes 50–76 minutes. The macOS job sets the end time: 46–70 minutes, while every other job finishes by about 45 minutes. Two causes dominate:

  1. The shared Rust compiler cache stores nothing. Every job compiles about 1,800 Rust crates cold, on every event.
  2. The macOS job runs four long phases one after another: source-home build, model journey, two Runtime builds, and the update journey.

Fix the cache first; the other estimates depend on it.

Evidence

Runs measured: develop push 37902075974 (49m50s) and PR 37906858619 (76m08s), both on 9 October.

Job PR run develop run
source-gate (all jobs wait for it) 5.3 min 3.8 min
source-home-macos 70.2 min 45.9 min
source-home-linux x86-64 / ARM64 33.5 / 31.9 min 40.8 / 32.4 min
test-capsules 26.1 min 26.4 min
custody-harness-smoke 23.8 min 23.7 min
test-elastos 20.6 min 15.9 min

macOS job steps (PR / develop): source-home 41.1 / 26.4 min, two Runtime versions 15.9 / 9.4 min, model journey 5.1 / 3.6 min, update journey 5.8 / 5.0 min. The same steps vary by 24 minutes between runs, which points to macos-14 runner speed (3 cores).

Compiler cache.

  • Rust hit rates, the same in both runs:
    • source-home-macos: 2 hits and 1,816 misses (0.11 %);
    • source-home-linux x86-64: 4 hits and 1,720 misses (0.23 %).
  • All cache error counters are 0, and "Average cache read hit" is 0.000 s. C/C++ hits are about 50 %, which matches repeated compiles in the job's private disk tier.
  • The repository cache list (GET /repos/Elacity/elastos-runtime/actions/caches) holds 34 entries using 8.9 GB of the 10 GB cap. None of them comes from sccache. lint, test-elastos and test-capsules use the same action in single-level GHA mode and print no statistics; they add no sccache entry either.
  • The develop push is a writer: CI_SAVE_CACHE is true (ci.yml:35) and the action sets READ_WRITE (.github/actions/rust-compile-cache/action.yml:11). A writer runs, but no entry persists.
  • CI and tests: warm caches, fail fast, keep only what catches real bugs #194 (8 October, 18:56) expected the first PR after develop run 37828101157 to read a warm cache. Both runs above came after it and read nothing.
  • scripts/ci-rust-cache-smoke.py proves local reuse and read-only GHA lookups in a fixture. It does not prove that a develop push stores remote entries (CI and tests: warm caches, fail fast, keep only what catches real bugs #194, finding 8).
  • About 4.8 GB of the cache sits in entries last read before 7 October, 03:05 UTC:
    • ubuntu-22.04-arm-ARM64-elastos-bd-source-home-linux-… 2,561 MB;
    • Linux-elastos-test-bd-…-50088228-… 1,388 MB;
    • Linux-elastos-bd-lint-…-50088228-… 456 MB;
    • the 50088228 capsules entry, the macOS-elastos-bd-… entry, two kubo-macOS-ARM64-… entries (one in refs/pull/271/merge) and providers-bd-target-….

Work

Each item is one PR. Item 1 comes first. Items 2–6 are independent of each other, and item 7 comes last.

1. The compiler cache stores and serves entries

Find why no sccache entry persists. Test these candidates in order:

  • (a) The GHA backend's write path under cache service v2 (ACTIONS_RESULTS_URL).
  • (b) The source-home multi-level chain disk,gha with SCCACHE_MULTILEVEL_WRITE_ERROR_POLICY=l0, which can hide level-1 write errors ("set job build directories" step in source-home-linux and source-home-macos).
  • (c) Eviction under the 10 GB cap.

Diagnose with one develop dispatch that sets SCCACHE_LOG=debug and SCCACHE_ERROR_LOG, and uploads the log as an artifact. Then list caches by the sccache- key prefix.

Remove the stale entries listed above. Agree a storage budget per cache family with Anders (#194, finding 8).

  • A develop push stores sccache entries. Proof: the cache list after the run shows sccache-* keys in refs/heads/develop.
  • The next PR run shows a Rust hit rate of 70 % or more in source-home-macos and source-home-linux. Proof: the sccache statistics in the job summary.
  • Total cache use stays inside the agreed budget for three consecutive develop pushes, and current entries stay in the cache. Proof: the cache list after each push.

2. The macOS update hop runs as its own job

Today source-home-macos runs the source-home build, the model journey, "build two actual Runtime versions", fixture generation and the update journey in sequence. A separate update-hop-macos job needs a qualified source-home Runtime and support inputs as well as the pinned previous release: source-gate alone does not produce them. Choose a commit/tree-bound producer handoff or an independent build with qualified support. Both jobs use the same accepted source, toolchain and journey profile. Preserve exact N/N+1 stamps, the previous-release pin, refusal checks and Undo (#194, finding 6). Measure both the critical path and additional runner/artifact cost after the compiler-cache baseline is proved.

  • The macOS critical path equals the longer of the two jobs. Proof: three PR runs.
  • The hop job still proves install N, update to N+1, the refusals and Undo. Proof: its job summary.
  • Anders adds the new job to the required checks. Branch protection is his setting.

3. Heavy jobs start without waiting for source-gate

Every heavy job waits for source-gate, which takes about 4–5 minutes. Starting builds earlier can reduce elapsed time and can spend runner minutes on candidates that fail the cheap gate. Keep the current dependency until measurements show that early start improves the agreed cost and latency goals. If adopted for pull requests, retain source-gate as a required check and prove cancellation at its first failure. Push and merge-group runs need their own cancellation evidence.

  • On a PR, the heavy jobs start within one minute of the run. Proof: the job timeline.
  • A failing source-gate cancels the PR run. Proof: one deliberate test PR.

4. Capsule workspace throughput is measured and improved

The 25 capsule workspaces already share intermediates through the repository Cargo build directory. First measure Cargo startup, compilation, test execution and shared-directory lock wait. Batch compatible full-package selections within one workspace and identical profile/flags before adding concurrent Cargo processes. Separate capsule manifests require separate workspace calls. Compare a bounded worker count or matrix shards only when the measured bottleneck supports them. Keep warnings as errors, per-package counts, zero-test refusal, failure propagation and readable logs.

  • test-capsules finishes in 12 minutes or less. Proof: three runs.
  • A failing capsule still fails the job, and the log names the capsule. Proof: one deliberate test PR.

5. The custody image build uses prebuilt or cached Rust layers

The image prebuild takes 17 of the 24 minutes (ci.yml:432-457; deploy/custody-host/Dockerfile). Choose one approach:

  • Option A: a cargo-chef dependency layer, so dependency compilation survives source changes.
  • Option B: build the binaries on the runner with the compiler cache, and COPY them into the image.

Either way, pin FROM rust:1.91-bookworm (Dockerfile:21) by digest, as the ARM engine container already is.

  • A warm prebuild takes 6 minutes or less. Proof: three runs.
  • The base image is pinned by digest. Proof: the Dockerfile and a policy test.

6. test-elastos uses nextest

cargo test takes 12–16 minutes. #194 Part 2 already targets the two 3-minute tests and the 17-minute goal; this item adds process-level parallelism. Compare existing compatible Cargo batching with a pinned, checksummed cargo-nextest experiment. Adopt a new runner only after process isolation, CPU/memory limits and expected fixture failure behavior pass. Keep doctests with cargo test --doc and retain the exact case inventory. Test count equality alone does not prove equivalent behavior.

  • The test count is equal before and after. Proof: cargo nextest list against cargo test -- --list.
  • test-elastos finishes in 12 minutes or less. Proof: three runs.

7. Documentation-only PRs skip the heavy matrix safely

Safe documentation-only changes can use an in-job skip that still reports each required check. Markdown and documentation may also be embedded product or build inputs, so file extensions alone cannot qualify a skip. Implement the input-coverage work in #317 first. Then prove negative cases for embedded Markdown, installer/release inputs, product data and mixed changes. Preserve formatting and product-data gates. Public PR #326 narrows the local gate; the hosted matrix still needs this separate proof.

  • A docs-only PR completes in 10 minutes or less. Proof: one test PR.
  • The policy test refuses a skip filter that matches any non-documentation path. Proof: the test.

Lower priority

  • cancel-on-failure holds a runner for the whole PR run. A longer polling interval reduces API requests while the runner remains allocated. Runner-minute savings require a different cancellation design; keep write authority outside build jobs.
  • A larger macOS runner (macos-14-xlarge) shortens items 1–2 further, at extra cost. This is Anders' budget decision.

Security and consistency rules for every item

Decision for Anders: one CI run per tree (merge queue)

Anders decided on 8 October to keep the merge queue off for now. This section records the current evidence for the next review of that decision.

  • develop protection: required_status_checks.strict is false, required_pull_request_reviews is null, and the nine checks are required. main: strict is true and reviews are required.
  • Ruleset "develop merge queue (CI and tests: warm caches, fail fast, keep only what catches real bugs #194)" is disabled.
  • scripts/release-publish.sh:55-75 (checked_ci) now accepts a successful merge_group run for the exact package SHA, so the release gate is ready for queue evidence.
  • From 6 to 8 October, 13 of 26 develop push CI runs ended cancelled and 13 succeeded (gh run list --workflow ci.yml --event push --branch develop --created 2026-10-06..2026-10-08). A cancelled develop run leaves its tree without the develop CI evidence that prepare requires.
  • Anders' 8 October measurement: about 1,545 of 5,622 job-minutes in 24 hours re-proved a tree that CI had already tested.
  • Because strict is false, two PRs that each pass against an older base can merge and break develop together. Their combined tree is first tested on the develop push.

Decision requested: enable the queue with lighter PR runs (branch ci/one-run-per-tree, 63bedd01), or keep the current setup and record the accepted risk.

Target

These estimates need measurement:

  • Today: 50–76 minutes.
  • After item 1: about 30–40 minutes.
  • After item 2: about 25–30 minutes.
  • After item 3: about 20–25 minutes.

Items 4 and 5 then keep the Linux jobs below that line.

How to work on this (any person or agent)

  • Read AGENTS.md first. Follow its rules for branches, reviews, push approval and the acceptance record.
  • Branch from current develop as fix/<slug>, one PR per item. Workflow changes conflict easily; check open PRs that edit ci.yml (for example CI and tests: warm caches, fail fast, keep only what catches real bugs #194's batches) before you start.
  • Line numbers refer to develop 8941d478. If lines moved, find the code by the quoted step or job name.
  • Local checks: just ci-local-prepush. Workflow edits also need python3 scripts/ci-release-policy-test.py and the pinned actionlint.
  • Proof is CI evidence: link the run IDs and quote the job-summary numbers. Keep one acceptance record in this issue, as AGENTS.md describes.

Activity

  1. andersalm commented on Oct 9, 2026

    @andersalm
    Contributor

    Merge queue stays off: trains merge one at a time and develop runs are never cancelled (ci.yml concurrency is per run on push). Order: the advisory's cache-credential fix first, then item 1 (prove a develop write and a warm PR read); items 2-7 are measured in #194. The OMP operator owns this.

  2. added this to the 0.8.0 milestone on Oct 9, 2026
  3. andersalm commented on Oct 9, 2026

    @andersalm
    Contributor

    The current cache failure has a concrete source cause. The cache action starts sccache from a shell after exporting the cache URL and token, but it omits the runner's cache-service-v2 flag. The pinned backend can then choose retired v1 endpoints. Develop run 37943218922 reports 941 cache write errors for 941 compilations in lint; the source-home multilevel checks obscure the remote failure. The existing smoke fixture supplies the v2 flag itself, so it does not reproduce production startup.

    The private Group A candidate in #311 is being changed to start the daemon with the runner's complete cache environment and keep routine build-step environments free of the runtime credential. Focused fixtures will cover this startup boundary. Acceptance still needs a real trusted write followed by a separate warm read; local fixtures alone will not close item 1. The next speed measurements will use that corrected baseline.

  4. andersalm commented on Oct 9, 2026

    @andersalm
    Contributor

    A fresh source and timing check on develop 7184e044 and run 37943218922 attempt 1 refines the next efficiency work.

    Item 5: custody Docker compilation is separate from the Runtime compiler-cache action. The pre-build took 13m43s and the smoke took 5m07s. The Dockerfile copies source before its Cargo builds and uses cache mounts. Docker's cache-mount documentation confirms that GHA layer export alone does not persist those mounts. Add trusted mount persistence or a reusable dependency layer, then measure a changed-source warm build. Preserve the smoke.

    Item 7: #326 repairs local gate scope, but remote PR CI still needs safe documentation-only selection. Complete #317's input coverage first. Embedded documentation and product inputs must select their consumers, and every required check must report a result.

    Item 2: the Mac job took 76m40s: source-home 50m01s, model journey 4m52s, two version builds 13m55s, and update hop 5m42s. A job split can remove part of the serial tail only if a qualified source/support producer feeds both consumers. build-ci-hop consumes source-home Runtime and support bytes, so a new job that needs only source-gate is insufficient. Preserve SHA/tree bindings, N/N+1 versions, previous-release pin, refusal and Undo proof. Measure added runner and artifact cost after the corrected cache baseline.

    Current develop already skips Mac packaging on ordinary PR/develop runs, separates Linux x86 from the ARM engine, and disables release LTO in journey builds. Keep those improvements. The cache startup correction remains in #311's private candidate; trusted remote-write and warm-read acceptance is still pending.

  5. andersalm commented on Oct 9, 2026

    @andersalm
    Contributor

    Batching follow-up from the current local source gate: separate work selection from Cargo process launch. The gate launches unit commands per package; different dependency-feature graphs can require additional compilation even when the same workspace has a qualified cache. Before adding process parallelism or a new runner, collect compatible full-package test targets and measure one workspace/profile/flags batch against the current launches. Keep module-filtered runs separate where their selectors differ. Acceptance must preserve a passing count for each selected package, zero-test refusal, all required unit targets and failure propagation.

    This is a measured experiment to take after the compiler-cache baseline, with measured results as its acceptance gate. Record compilation time, test time and build-directory lock waits. Capsule workspaces have separate manifests; sharing their intermediates does not make simultaneous Cargo commands bypass the shared-directory lock.

  6. andersalm commented on Oct 9, 2026

    @andersalm
    Contributor

    A fresh public PR/comment check on 9 October confirms that the current timing evidence still needs careful comparison.

    For batching, start with compatible packages in one workspace and profile. Measure compilation, execution and lock waits separately. Run new regressions first with explicit selected-test counts, then retain complete candidate acceptance. Each timed selector needs an exact intended test and a refusal when selection is empty.

    Keep the current order: approved advisory integration and measured cache acceptance; #317 input closure; the owner-led #225 timing repair; then qualified Mac handoffs, custody layers and review packages. The unchanged installer suite's128-second idle measurement and repeated transport matrix are recorded in #194.

  7. modified the milestones: 0.8.0, After 0.8.0 on Oct 12, 2026
  8. andersalm commented on Oct 13, 2026

    @andersalm
    Contributor

    Postponed to 0.8.1: CI cleanup/hardening; no workflow or deletion changes on release day.

  9. andersalm commented on Oct 19, 2026

    @andersalm
    Contributor

    CI state on 19 Oct, measured on source 3a043849e (analysis relayed by Anders). This replaces the older "the compiler cache stores nothing" diagnosis.

    • The cache works on warm paths. The Mac job took 28m25s on the develop push (1,814 hits / 0 misses). It took 66m05s on the manual train dispatch (6 / 1,911) and 64m20s on the release PR, attempt 2 (2 / 1,812). The cold paths come from cache scoping across refs and events, which needs a targeted check of producer scope and restore keys.
    • Scheduling delay: in the warm develop run, the Mac job started about 18 min after its prerequisite finished; the whole run took about 74 min.
    • Reliability (last 100 workflows, 17–19 Oct): 95 CI runs, of which 23 succeeded, 69 were cancelled, 2 failed and 1 was running. 21 of the cancelled runs contained failed jobs, 16 of them a Linux x86-64 source-home failure (Installed journeys in source-home-linux (ubuntu-24.04) fail intermittently, on develop too #433). Fail-fast turns failures into "cancelled".
    • Duplicate qualification: 3a043849e got a manual train dispatch at 08:05:19, develop CI at 08:05:32 and release-PR CI at 08:05:35. The manual run used about 316 job-minutes. Publication only accepts develop push or merge-group evidence.

    Plan (OMP, within the 0.8.0 batch approved on #93):

    1. Now: the push queue stops dispatching CI for intermediate release-train pushes. One dispatch runs on the final alpha.25 head only, because only workflow_dispatch runs the Browser helper source builds outside packaging and the installed journeys with the shipped LTO profile (ci.yml:636-648, :751-754; 6266619339).
    2. In the batch: model-provider: every held-lock journal test takes its lock with the bounded retry #419, test(people): the first-run layout smoke waits for the rendered Profile text #422, and the CI diagnostics and vendored licence from ci: installed journeys name the observer failure, and the SmolLM2 proof uses a vendored license #413, after deps: move zip to 9.x for RUSTSEC-2026-0367/0368/0369 #417 resolves its zip dependency.
    3. After 0.8.0: an early local tool-version check, a docs-only fast path (CI and tests: warm caches, fail fast, keep only what catches real bugs #194), cold-cache repair on the release paths, and reconciling CI speed: make the compiler cache store entries and shorten the macOS path #310/Required checks run whenever their inputs change (path filters) #317/The local pre-push gate refuses what CI refuses #319/CI proves install and update on Linux #320 with this state. Starting heavy jobs early and splitting the Mac job stay deferred until warm execution, scheduling and failures are measured separately.
  10. andersalm commented on Oct 19, 2026

    @andersalm
    Contributor

    OMP changed the push queue to skip unmarked train dispatches and accept a full-HEAD marker for the final candidate. Independent static reads match SHA-256 cbcd6c9057a10236ca0607468be2d56fe407787ca6c18754213f074642636b62; the replacement-worker receipt is OMP’s observation.
    The marker records eligibility. Dispatch still resolves a branch name; a failed dispatch still removes the queue entry, while an unchanged re-enqueue exits through the already-pushed check. Bind the final alpha.25 run to its exact source, run and attempt, and retain a recovery path for a failed dispatch before claiming that coverage.
    The final dispatch retains the distinct shipped-profile journeys and Browser helper builds. Current develop CI and package preparation remain separate evidence; this static check ran neither the queue nor CI.

  11. andersalm commented on Oct 19, 2026

    @andersalm
    Contributor

    The completed Hosted gate at f37aa1219 reports 1,691 passing cases; 49 verdicts from the ends of two unchanged JS files are absent compared with b5847af0e (1,740). The Claude review reconciles 42 product-data and 7 Browser metrics labels, with local file runs of 99 and 26 cases.
    Node #64833 reports the same --test-force-exit pattern; fix #64875 remains open. This supports the reporting hypothesis; the reviewer could not reproduce this occurrence locally, so the remote pass count remains a lower bound.
    Keep the reporting repair in the After 0.8.0 CI lane: choose bounded process cleanup or complete-result accounting, and prove that both missing results and failed cases make the gate fail. Hosted source review PASS and installed Mac acceptance remain separate (#85).

  12. andersalm commented on Oct 19, 2026

    @andersalm
    Contributor

    The installed test tools now take and hold the current remote check lock before teardown. The completed opslease1 source review passed; its probes used copies and contacted no real boxes.
    Independent installed SHA-256 checks match elastos-remote-check (f1bc351a8bc5eae08b81ed93f95b55eb6e939d4b77deb729be8fa80d36628adf) and elastos-remote-free (e2334c8559841313200dbeb0cb2b7821830fac54752143a4091eebbbec5120b3). Elapsed-time limits now use monotonic time.
    These source changes close the recorded ownership and clock limits. The active train gate loaded the earlier tools; a real run of the new tools and current candidate acceptance remain unproven.

  13. andersalm commented on Oct 20, 2026

    @andersalm
    Contributor

    OMP correction for the remote-check slot ownership findings (Codex root, 19 Oct; local ops tools, not repo code):

    • One slot lease from selection through teardown: elastos-remote-check takes ~/.local/share/elastos-ops/slot-N.lock (flock) before it picks a box and holds it to exit. A second check on the same slot is refused ("slot N is in use by another check on this Mac").
    • No destroy after @@BUSY: a check that finds the box held by another check keeps the box ("kept: another check holds it").
    • Bounded waits: every ssh call has a timeout (10 min for upload/start, 2 min per poll). A missing output file counts as a lost poll (10 min limit). No output and no exit code for 45 min ends the run.
    • elastos-remote-free SLOT: takes the same local lease, then a holder on the box takes the box's check lock and keeps it until the box is destroyed, so nothing can start between the probe and the deletion.
    • Verified:
      • with the slot-9 lease held, both tools refuse;
      • on a probe box with a fake check holding the lock, remote-free refused (exit 1);
      • after the fake check released the lock, it destroyed the box (exit 0), leaving 0 boxes.
      • The detached-and-polled run passed the train gate on fcdb1f651.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions