Skip to content

perf(ci): pin Rust deps with a GC root so the sticky disk keeps them - #1274

Merged
ryoppippi merged 1 commit into
mainfrom
ci/persist-deps-cache
Jun 11, 2026
Merged

ryoppippi merged 1 commit into
mainfrom
ci/persist-deps-cache

Conversation

@ryoppippi

@ryoppippi ryoppippi commented Jun 11, 2026 •

Copy link
Copy Markdown
Member

Summary

The linux native build rebuilds the entire crate dependency set (~190s) on
every run — including on main — even though the ccusage-deps derivation
hash is identical across runs (verified from the build logs). So the deps are
deterministic but never served from cache.

Root cause

crane's cargoArtifacts (deps-only) store path has no GC root once the final
binary is built. The Blacksmith sticky disk appears to drop unreferenced paths
on commit, so the deps are gone by the next run and get fully recompiled.

Fix (hypothesis to validate in CI)

  • Expose the static build's cargoArtifacts via passthru.
  • After nix build .#ccusage-static, pin the deps with a GC root on the mounted
    Nix store (/nix/ccusage-ci-gcroots/deps). The root lives on the sticky disk,
    so the deps survive the commit and the next run reuses them.

How to verify

After this merges, the second build-linux-* run on main should drop from
~190s to ~40s (deps reused, only the final binary recompiles). If it does not,
the sticky disk is dropping paths for a different reason (e.g. not committing the
full store) and we'll need to look at the Blacksmith disk config instead.

Same root cause affects the test job's cargoTest (#1273) and macOS; if this
works, I'll roll the GC-root pin out to those too.


Summary by cubic

Pins Rust dependency artifacts with a Nix GC root so the Linux native build reuses crane cargoArtifacts across runs instead of rebuilding. Exposes ccusage-static’s cargoArtifacts via passthru and in CI links them to /nix/ccusage-ci-gcroots/deps with nix build '.#ccusage-static.cargoArtifacts', targeting ~190s → ~40s on the second run.

Written for commit c766768. Summary will update on new commits.

Review in cubic


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

Chores

  • Build Pipeline: Optimized dependency caching mechanisms in Linux native package builds to enhance build performance and reliability across continuous integration runs.

The linux native build rebuilt the entire crate dependency set (~190s) on every
run, even though the `ccusage-deps` derivation hash is identical across runs.
The crane cargoArtifacts path has no GC root once the final binary is built, so
the Blacksmith sticky disk drops it on commit and the next run recompiles all
deps from scratch.

Expose the static build's `cargoArtifacts` via passthru and, after the build,
pin it with a GC root under the mounted Nix store
(`/nix/ccusage-ci-gcroots/deps`). The root persists with the sticky disk, so the
deps stay warm and subsequent runs should reuse them instead of rebuilding.
@pullfrog

pullfrog Bot commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

This run was cancelled 🛑

The workflow was cancelled before completion. Please check the link below for details.

Pullfrog  | View workflow run | via Pullfrog | 𝕏

@ryoppippi
ryoppippi merged commit c764998 into main Jun 11, 2026
13 of 14 checks passed
@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4545ed10-6391-42ac-800a-3165881e1917

📥 Commits

Reviewing files that changed from the base of the PR and between 42e5b16 and c766768.

📒 Files selected for processing (2)
  • .github/actions/build-linux-native-package/action.yaml
  • nix/static-package.nix

📝 Walkthrough

Walkthrough

This PR adds Nix GC root pinning for Cargo dependency artifacts in CI. The ccusage-static Nix derivation now exports its dependency artifacts via passthru.cargoArtifacts, and the build-linux-native-package GitHub Action adds a step to pin those artifacts with a persistent GC root, preventing dependency cache misses across runs.

Changes

Cargo dependency cache persistence

Layer / File(s) Summary
Cargo artifacts export and GC root pinning
nix/static-package.nix, .github/actions/build-linux-native-package/action.yaml
The ccusage-static derivation exposes cargoArtifacts via passthru, and a new CI action step creates a persistent Nix GC root at /nix/ccusage-ci-gcroots/deps linking to those exported artifacts, with conditional NIX_CONFIG adjustment when nix-github-token is disabled.

Possibly related PRs

  • ccusage/ccusage#1260: Modifies the same build-linux-native-package action around nix build invocation and conditions.
  • ccusage/ccusage#1266: Updates Nix CI to reuse shared cargoArtifacts outputs for cached builds in the pipeline.

Suggested reviewers

  • pullfrog

Poem

🐰 A rabbit hops through Nix with glee,
Pinning artifacts so they stay free,
GC roots keep caches warm and bright,
No rebuilds in the endless night! 🌙


🎯 2 (Simple) | ⏱️ ~8 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/persist-deps-cache

Comment @coderabbitai help to get the list of available commands and usage tips.

@ryoppippi
ryoppippi deleted the ci/persist-deps-cache branch June 11, 2026 15:56
@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: c76676808959
Base SHA: 42e5b168cf0b

This compares the PR package against the configured base package on the same CI runner.

Package runner startup

Execution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one bunx -p <url> ccusage --version run with an empty Bun install cache. Warm reuses that cache and reports the median of repeated runs.

Package SHA Execution setup Bunx temp cache Bunx warm median Warm samples
Base pkg.pr.new 42e5b168cf0b 973.0ms 941.7ms 67.3ms 3
PR pkg.pr.new c766768 988.0ms 1.063s 62.3ms 3

Cached bunx execution performance

Runs the same large fixture through bunx -p <pkg.pr.new URL> ccusage after the Bun install cache has already been populated by the startup measurement. This separates cached package-runner execution from first-fetch package materialization.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base package: 42e5b168cf0b; PR package: c766768. Both run through bunx -p <pkg.pr.new URL> ccusage using the warmed Bun install cache from package runner startup, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
bunx -p <pkg> ccusage claude --offline --json 1.01 GiB 905.1ms 965.1ms 0.94x 720.75 MiB 732.75 MiB 1.02x 1.11 GiB/s 1.04 GiB/s
bunx -p <pkg> ccusage codex --offline --json 1.01 GiB 195.5ms 201.7ms 0.97x 91.75 MiB 89.75 MiB 0.98x 5.15 GiB/s 4.99 GiB/s

Package runtime diagnostics

Compares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 924.6ms 1.09 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 971.3ms 1.04 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 176.8ms 5.69 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 138.9ms 7.25 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 48.8ms 42.2ms 1.16x 43.25 MiB 43.00 MiB 0.99x 0.03 MiB/s 0.04 MiB/s
claude session --offline --json 0.00 MiB 43.7ms 40.9ms 1.07x 43.25 MiB 43.00 MiB 0.99x 0.04 MiB/s 0.04 MiB/s
codex daily --offline --json 0.00 MiB 43.0ms 42.3ms 1.02x 43.25 MiB 43.25 MiB 1.00x 0.02 MiB/s 0.02 MiB/s
codex session --offline --json 0.00 MiB 42.2ms 43.5ms 0.97x 43.00 MiB 43.00 MiB 1.00x 0.02 MiB/s 0.02 MiB/s

Large real-world-shaped fixture performance

Generated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 953.7ms 892.8ms 1.07x 730.75 MiB 745.75 MiB 1.02x 1.06 GiB/s 1.13 GiB/s
codex --offline --json 1.01 GiB 181.9ms 176.6ms 1.03x 89.00 MiB 91.50 MiB 1.03x 5.53 GiB/s 5.70 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 17.32 KiB 17.33 KiB +0.00 KiB 1.00x
installed native package binary 3324.84 KiB 3324.84 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: c76676808959
Base SHA: 42e5b168cf0b

This compares the Rust PR release binary against the configured base package on the same CI runner.

Package runner startup

Execution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one bunx -p <url> ccusage --version run with an empty Bun install cache. Warm reuses that cache and reports the median of repeated runs.

Package SHA Execution setup Bunx temp cache Bunx warm median Warm samples
Base pkg.pr.new 42e5b168cf0b 1.335s 861.8ms 73.7ms 3
PR pkg.pr.new c766768 961.2ms 992.5ms 74.9ms 3

Cached bunx execution performance

Runs the same large fixture through bunx -p <pkg.pr.new URL> ccusage after the Bun install cache has already been populated by the startup measurement. This separates cached package-runner execution from first-fetch package materialization.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base package: 42e5b168cf0b; PR package: c766768. Both run through bunx -p <pkg.pr.new URL> ccusage using the warmed Bun install cache from package runner startup, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
bunx -p <pkg> ccusage claude --offline --json 1.01 GiB 931.1ms 960.5ms 0.97x 739.00 MiB 728.50 MiB 0.99x 1.08 GiB/s 1.05 GiB/s
bunx -p <pkg> ccusage codex --offline --json 1.01 GiB 226.2ms 217.4ms 1.04x 92.00 MiB 87.00 MiB 0.95x 4.45 GiB/s 4.63 GiB/s

Package runtime diagnostics

Compares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 971.3ms 1.04 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 959.1ms 1.05 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 200.6ms 5.02 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 150.7ms 6.68 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs rust/target/release/ccusage directly. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 56.1ms 7.1ms 7.90x 43.00 MiB 3.00 MiB 0.07x 0.03 MiB/s 0.22 MiB/s
claude session --offline --json 0.00 MiB 51.8ms 7.3ms 7.08x 43.00 MiB 2.75 MiB 0.06x 0.03 MiB/s 0.21 MiB/s
codex daily --offline --json 0.00 MiB 49.8ms 6.2ms 8.03x 43.00 MiB 2.75 MiB 0.06x 0.02 MiB/s 0.14 MiB/s
codex session --offline --json 0.00 MiB 53.5ms 7.3ms 7.35x 43.00 MiB 2.75 MiB 0.06x 0.02 MiB/s 0.12 MiB/s

Large real-world-shaped fixture performance

Generated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs rust/target/release/ccusage directly. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 868.4ms 910.8ms 0.95x 735.75 MiB 723.00 MiB 0.98x 1.16 GiB/s 1.11 GiB/s
codex --offline --json 1.01 GiB 179.8ms 147.0ms 1.22x 97.75 MiB 92.00 MiB 0.94x 5.60 GiB/s 6.85 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 17.32 KiB 17.33 KiB +0.00 KiB 1.00x
installed native package binary 3324.84 KiB 3324.84 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

ryoppippi added a commit that referenced this pull request Jun 11, 2026
Revert "perf(ci): pin Rust deps with a GC root so the sticky disk keeps them (#1274)"

This reverts commit c764998.
ryoppippi added a commit that referenced this pull request Oct 14, 2026
* ci(release): shallow-clone the tag in native build jobs

The native build jobs checked out the release tag with fetch-depth: 0,
but nothing in the build path reads git history: CI builds the same
outputs from a default shallow checkout. The full clone cost up to 41s
on build-windows-x64 in run 38470996208, which finished within 20s of
the slowest leg.

* ci: keep the Linux Nix store on a Blacksmith sticky disk

The cache-nix-action store cache misses most of the time. Across ~25
CI pushes to main between 2026-09-25 and 2026-10-13, the native build
legs restored an entry in only 3-6 of ~19 runs each (macOS and Linux
alike), and a hit roughly halves the build step (linux-arm64: 82s vs
213s). Blacksmith's Actions cache allows 25 GB per repository per week
with LRU eviction, while the per-commit keys introduced in #1296 write
a fresh 1.3-3 GB entry for every job on every run: ~12 GB per push to
main, ~6 GB per PR run, up to 2.5 GB per hourly update-pricing run.
#1296 assumed the backend was effectively unlimited; it is not. Even
on a hit, restoring takes ~60s and the post-job save another ~40s.

On Blacksmith Linux runners, mount /nix from a sticky disk instead. A
job gets a copy-on-write clone of the last committed snapshot, so the
whole store is present at mount time with no restore or save step and
nothing counted against the cache quota. macOS and GitHub-hosted
runners have no sticky disks and keep using cache-nix-action.

#1296 dropped an earlier sticky-disk setup. This one avoids its
problems:

- Keys are per job and matrix leg but carry no workflow name, so
  release.yaml's build-native-packages mounts the disk ci.yaml keeps
  warm on every push to main instead of starting cold. autofix, which
  only runs on pull requests and so would never commit, borrows
  preflight's disk via the new disk-job input; both build the treefmt
  closure.
- The old trim dropped Crane's cargoArtifacts because nothing rooted
  them (#1274, #1279). The Linux build now roots them inside
  /nix/var/nix/gcroots, which persists with the disk, and
  keep-derivations/keep-outputs make GC keep their build-time closure
  (toolchain, vendored sources) too.
- The weekly GC workflow (#1295) mounted its own disk and never touched
  the real ones. Trimming now happens in the job that commits: once the
  disk is over half full, `nix-collect-garbage --delete-old` runs before
  the job adds to the store. Jobs without persistent roots go cold once
  per trim.

Only runs on main that are not pull_request(_target) events commit,
whatever `save` says, so pull request code (including forks) can read
main's store but never write the store that main and releases build
from. cache-nix-action keeps its existing behaviour, now also gated by
`save`.

Not confirmed from Blacksmith's documentation: the disk's capacity
(hence the relative threshold and the `df` log on every run) and which
snapshot wins when two runs commit the same key concurrently. Either
outcome is a valid store for a cache.

* ci(release): stop persisting the Nix store from release builds

Release builds now mount the Linux sticky disk that CI commits on every
push to main. Committing it from the release as well would only race
those commits with an equivalent store.

On macOS, the release's own cache-nix-action entries never survive to
the next release under Blacksmith's cache eviction, yet each save took
~40s (build-mac-arm64 and build-mac-x64 in run 38470996208) before the
job could finish and let the npm job start.

Add a save-nix-cache input to both native build actions, forwarded to
setup-nix-cache, skip rooting the Crane artifacts when nothing will be
saved, and pass save-nix-cache: false from release.yaml.

* ci: mount test's Nix sticky disk in the perf comment legs

ccusage-perf-comment only runs on pull requests, and pull requests
never commit a sticky disk, so its own per-leg keys would stay freshly
formatted on every run. Mount the disk test commits on every push to
main instead: it installs `just` and resolves the same flake inputs.

Both legs mount it read-only, so the per-runtime cache-scope that kept
their saves apart is no longer needed.

Reported by pullfrog in review.
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