Skip to content

ci(nix): make the macOS Nix cache save a trimmed store with the Crane layers - #1921

Merged
ryoppippi merged 4 commits into
mainfrom
ci/fix-macos-nix-cache
Oct 18, 2026
Merged

ryoppippi merged 4 commits into
mainfrom
ci/fix-macos-nix-cache

Conversation

@ryoppippi

@ryoppippi ryoppippi commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Summary

The macOS legs of build-native-packages haven't saved a Nix store since #1882. So every macOS build restores the same stale entry and rebuilds the whole Crane chain. This PR makes main save a GC-trimmed store that holds the Crane layers. It also lets release builds restore that store and drops rust-docs from the toolchain.

Evidence

  • No saves since 2026-10-14. All 24 build-mac-arm64 runs on pushes to main from 6ca2776 (ci: keep the Linux Nix store on a Blacksmith sticky disk #1882, 2026-10-14T11:14Z) to f6d28c5 restored the same key, nix-CI-build-native-packages-build-mac-arm64-05c2a29d…-75d48e98…. Run 38547180232 wrote that entry, and it was the last run to save. In every later run, the post-job Cache Nix store step finished in about 100 ms with no output (e.g. job 117682344381 in run 39129740960). In cache-nix-action v7.0.2, the only silent exit from saveImpl is inputs.save not being "true". ci: keep the Linux Nix store on a Blacksmith sticky disk #1882 started passing save: ${{ inputs.save }} through build-macos-nix-native-package -> setup-nix-cache, and the nested post step reads it as empty. Before ci: keep the Linux Nix store on a Blacksmith sticky disk #1882 the input fell back to its "true" default and the save worked. The release npm job, which nests only one level, still saves today (nix-tagpr-npm-…).
  • Cost per run. The restore downloads 1.4 GB in ~2 s, then spends ~48 s in sudo gtar -x. The deps hash in the key has moved on since then, so Crane rebuilds deps (~20 s), foundation (~9 s), adapters (~5 s), and ccusage (~25 s).
  • Release macOS builds are always cold. The release key starts with nix-tagpr-…, and save-nix-cache: false means nothing ever writes it. In the v20.0.35 release (run 39053076844, build-mac-arm64), the restore found no cache. The build step built 161 derivations, including the toolchain, fetched 185 paths (1.8 GiB unpacked), and took 139 s.
  • The cached store keeps growing. The macOS save never collected garbage (Not collecting garbage because none of "gc-max-store-size", "gc-max-store-size-macos" are specified), so old Crane layers and binaries pile up in each entry.
  • rust-docs is in every toolchain. rust-toolchain.toml sets no profile, so rust-overlay uses default, which includes rust-docs-1.97.1-aarch64-apple-darwin: 728 MB and 65,585 files. That's about half the files in the macOS build closure (measured locally on the cacheRoot closure).
  • Cache location. The macOS entries live in Blacksmith's Actions cache: they don't appear in gh cache list, and restores run at ~700 MB/s. So they don't count toward GitHub's 10 GB quota. That quota is currently over its limit (11.97 GB): ~1.6 GB nix-tagpr-npm entries and Windows ARM64 cargo entries. Those entries are out of scope here.

Changes

  1. ci(nix): drop the workflow name from the Nix cache key: the key is now nix-<job>-<scope>-<deps hash>-<sha>, the same scheme as the sticky disk key. Release builds run on pushes to main, so they restore CI's newest main entry, often for the same commit, and they still never save.
  2. ci(nix): save the macOS Nix store explicitly after rooting its closure: setup-nix-cache gets a defer-save input plus primary-key and save outputs. For macOS it only restores (cache-nix-action/restore), and build-macos-nix-native-package then:
    • roots the Crane cacheRoot and the flake inputs this build fetched in /nix/var/nix/gcroots/ci. It finds the inputs with nix flake archive --dry-run, so unused pins aren't downloaded. The Linux build roots cacheRoot the same way.
    • saves with cache-nix-action/save as an ordinary step, only on pushes to main, with gc-max-store-size-macos: '0'. Everything that isn't rooted is collected, so each entry holds one build closure.
  3. build(rust): install the minimal rustup profile plus explicit components: removes rust-docs. clippy, llvm-tools-preview, and rustfmt are still listed explicitly.

How this will show up

Caches are saved only on pushes to main, so this PR's own run can't show the warm path:

  • This PR run: the macOS legs log the new key (nix-build-native-packages-build-mac-*-…) and Could not find a cache, so they build cold (about the release's 139 s build step). The Root current Crane artifacts and flake inputs step runs and logs Rooted N flake inputs, which checks the attribute paths on a real runner. Save Nix store is skipped. The build log should not list any rust-docs derivation.
  • First push to main after merge: cold build again, but then Save Nix store logs Collecting garbage, the store size before and after, and Saving a new cache with the key "nix-build-native-packages-build-mac-arm64-…-<sha>".
  • Every later run (PRs, main, release): the restore finds an entry from the latest main commit. For a PR that doesn't touch the Rust tree or pricing, the build should only rebuild what changed since that main commit. The ccusage-deps derivation should not show up in these N derivations will be built. The restore should get faster too, since the entry no longer has rust-docs or accumulated layers (the post-GC store size is in the save log). Release macOS builds should restore CI's entry for the release commit and build little or nothing.

Risks / notes

  • Every Nix-cached job, and the Windows cargo cache (its key hashes rust-toolchain.toml), restores cold once.
  • A main run now spends extra time on the macOS save (~40 s before ci: keep the Linux Nix store on a Blacksmith sticky disk #1882, plus GC). This doesn't affect PRs.
  • Release now reads cache entries from main. Only pushes to main save them, and Actions cache scoping keeps PR-scoped entries out of reach of main. That's the same trust model as the Linux sticky disk that release already mounts.
  • The rust-toolchain.toml profile change also affects the dev shell and rustup users (no local rust-docs). It can be split into its own PR if preferred.

PR run results (run 39132229428)

  • Restore: both macOS legs used the new key (nix-build-native-packages-build-mac-{arm64,x64}-e04fe334…-c77e9cba…) and logged Could not find a cache, as expected.
  • Build: cold, about 118 s for the build step and about 134 s per job. That's roughly what the old setup took (~135 s), even though the old setup restored a 1.4 GB entry first.
  • Root step: ran in about 1.2 s and logged Rooted 8 flake inputs.
  • Save step: skipped, because pull requests don't save.
  • rust-docs: no derivation for it appears in either build log.
  • test failure: ccusage-clippy fails on manual_range_patterns in adapters/antigravity/src/parser.rs:869. That failure comes from fix(antigravity): map model IDs 1418/1419 to gemini-4-argon #1905 and also fails on main (run 39129832136, job 117682623980). It isn't caused by this PR, and it shows clippy still runs with the minimal profile.

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ddbf43b5-b423-4656-8695-8c4323c63b19
📥 Commits

Reviewing files that changed from the base of the PR and between 6548615 and ad91367.

📒 Files selected for processing: 3
  • .github/actions/build-macos-nix-native-package/action.yaml
  • .github/actions/setup-nix-cache/action.yaml
  • rust-toolchain.toml
+-------------------------------+
| BOFH: Bunny of Friendly Help. |
+-------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ccusage-guide ad91367 Commit Preview URL

Branch Preview URL
Oct 18 2026, 03:25 PM

@pkg-pr-new

pkg-pr-new Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

ccusage

npx https://pkg.pr.new/ccusage@1921

@ccusage/ccusage-darwin-arm64

npx https://pkg.pr.new/@ccusage/ccusage-darwin-arm64@1921

@ccusage/ccusage-darwin-x64

npx https://pkg.pr.new/@ccusage/ccusage-darwin-x64@1921

@ccusage/ccusage-linux-arm64

npx https://pkg.pr.new/@ccusage/ccusage-linux-arm64@1921

@ccusage/ccusage-linux-x64

npx https://pkg.pr.new/@ccusage/ccusage-linux-x64@1921

@ccusage/ccusage-win32-x64

npx https://pkg.pr.new/@ccusage/ccusage-win32-x64@1921

commit: ad91367

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: a8fe8472320f
Base SHA: f6d28c5640bb

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

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, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 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 335.7ms 3.00 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 315.0ms 3.20 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 128.3ms 7.85 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 104.1ms 9.67 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 native ccusage binary 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 46.3ms 13.4ms 3.45x 55.50 MiB 22.98 MiB 0.41x 0.03 MiB/s 0.12 MiB/s
claude session --offline --json 0.00 MiB 39.6ms 20.9ms 1.90x 55.75 MiB 22.97 MiB 0.41x 0.04 MiB/s 0.07 MiB/s
codex daily --offline --json 0.00 MiB 43.1ms 13.4ms 3.23x 55.75 MiB 29.22 MiB 0.52x 0.02 MiB/s 0.06 MiB/s
codex session --offline --json 0.00 MiB 41.8ms 13.5ms 3.10x 55.50 MiB 29.22 MiB 0.53x 0.02 MiB/s 0.06 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, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published native ccusage binary 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 369.1ms 331.1ms 1.11x 961.10 MiB 971.11 MiB 1.01x 2.73 GiB/s 3.04 GiB/s
codex --offline --json 1.01 GiB 128.4ms 105.0ms 1.22x 527.44 MiB 535.44 MiB 1.02x 7.84 GiB/s 9.59 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 24.03 KiB 24.03 KiB +0.00 KiB 1.00x
installed native package binary 4907.03 KiB 4907.03 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: a8fe8472320f
Base SHA: f6d28c5640bb

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

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, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 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 376.4ms 2.67 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 345.8ms 2.91 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 168.1ms 5.99 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 125.7ms 8.01 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 40.2ms 42.0ms 0.96x 55.25 MiB 55.25 MiB 1.00x 0.04 MiB/s 0.04 MiB/s
claude session --offline --json 0.00 MiB 37.6ms 39.6ms 0.95x 55.50 MiB 55.75 MiB 1.00x 0.04 MiB/s 0.04 MiB/s
codex daily --offline --json 0.00 MiB 35.7ms 37.2ms 0.96x 55.25 MiB 55.50 MiB 1.00x 0.02 MiB/s 0.02 MiB/s
codex session --offline --json 0.00 MiB 36.8ms 39.2ms 0.94x 55.25 MiB 55.25 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, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 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 376.1ms 379.5ms 0.99x 897.10 MiB 945.11 MiB 1.05x 2.68 GiB/s 2.65 GiB/s
codex --offline --json 1.01 GiB 142.4ms 144.2ms 0.99x 533.43 MiB 525.45 MiB 0.99x 7.07 GiB/s 6.98 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 24.03 KiB 24.03 KiB +0.00 KiB 1.00x
installed native package binary 4907.03 KiB 4907.03 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: 6602f62d3a01
Base SHA: f6d28c5640bb

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

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, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 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 378.1ms 2.66 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 347.7ms 2.90 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 139.7ms 7.20 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 115.5ms 8.72 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 44.8ms 40.7ms 1.10x 55.75 MiB 55.50 MiB 1.00x 0.03 MiB/s 0.04 MiB/s
claude session --offline --json 0.00 MiB 37.7ms 33.6ms 1.12x 55.75 MiB 55.25 MiB 0.99x 0.04 MiB/s 0.05 MiB/s
codex daily --offline --json 0.00 MiB 39.8ms 38.0ms 1.05x 55.50 MiB 55.75 MiB 1.00x 0.02 MiB/s 0.02 MiB/s
codex session --offline --json 0.00 MiB 35.9ms 34.2ms 1.05x 55.75 MiB 55.75 MiB 1.00x 0.02 MiB/s 0.03 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, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 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 371.4ms 388.2ms 0.96x 949.10 MiB 937.11 MiB 0.99x 2.71 GiB/s 2.59 GiB/s
codex --offline --json 1.01 GiB 154.8ms 141.7ms 1.09x 515.44 MiB 517.44 MiB 1.00x 6.50 GiB/s 7.11 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 24.03 KiB 24.03 KiB +0.00 KiB 1.00x
installed native package binary 4907.03 KiB 4907.09 KiB +0.06 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: 6602f62d3a01
Base SHA: f6d28c5640bb

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

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, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 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 397.7ms 2.53 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 347.9ms 2.89 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 144.0ms 6.99 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 120.3ms 8.37 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 native ccusage binary 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 46.0ms 19.5ms 2.35x 55.75 MiB 22.98 MiB 0.41x 0.03 MiB/s 0.08 MiB/s
claude session --offline --json 0.00 MiB 37.4ms 13.5ms 2.77x 55.50 MiB 22.97 MiB 0.41x 0.04 MiB/s 0.11 MiB/s
codex daily --offline --json 0.00 MiB 37.2ms 13.6ms 2.73x 55.50 MiB 29.22 MiB 0.53x 0.02 MiB/s 0.06 MiB/s
codex session --offline --json 0.00 MiB 36.7ms 13.3ms 2.76x 55.75 MiB 29.23 MiB 0.52x 0.02 MiB/s 0.06 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, 2597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published native ccusage binary 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 382.5ms 370.5ms 1.03x 929.11 MiB 925.10 MiB 1.00x 2.63 GiB/s 2.72 GiB/s
codex --offline --json 1.01 GiB 143.2ms 122.5ms 1.17x 503.45 MiB 511.44 MiB 1.02x 7.03 GiB/s 8.22 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 24.03 KiB 24.03 KiB +0.00 KiB 1.00x
installed native package binary 4907.03 KiB 4907.09 KiB +0.06 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.

release.yaml builds the macOS packages with save-nix-cache: false, so
its keys (nix-tagpr-build-native-packages-...) never had an entry to
restore: the v20.0.35 release (run 39053076844, build-mac-arm64) found
no cache, built 161 derivations including the Rust toolchain, fetched
185 paths, and spent 139s in the build step.

Key the store by job and matrix scope only, like the sticky disk key.
Release builds run on pushes to main, so they now restore the newest
entry CI saved on main, often for the very commit being released, and
still never write one. The commit sha keeps workflows that share a job
id from blocking each other's saves; only runs for the same commit can
collide, and their stores hold the same build.

The key is computed once in a step and exposed through its outputs so a
later change can reuse it outside cache-nix-action's combined step.
Existing nix-CI-* and nix-tagpr-* entries stop matching, so each cached
job restores cold once.
No CI run has saved a macOS Nix store since #1882 (6ca2776). The
build-mac-arm64 leg of every push to main from 2026-10-14T11:14Z to
2026-10-18 (24 runs) restored the same entry,
nix-CI-build-native-packages-build-mac-arm64-05c2a29d...-75d48e98,
written by the last saving run (38547180232), and its post-job "Cache
Nix store" step finished in ~100 ms without a line of output. In
cache-nix-action v7.0.2 the only silent exit from the save is
`save` not being "true": #1882 started passing
`save: ${{ inputs.save }}`, and the post step of a cache-nix-action
nested in build-macos-nix-native-package -> setup-nix-cache evidently
reads it as empty. Before #1882 it fell back to the "true" default and
saved; the single-level tagpr npm job still saves today.

So every macOS build restored a four-day-old 1.4 GB entry (~50 s, 47 s
of it gtar extraction), found that the deps hash had moved on, and
rebuilt the whole Crane chain: deps ~20 s, foundation ~9 s, adapters
~5 s, ccusage ~25 s.

setup-nix-cache gains defer-save: it only restores, and exposes the
primary key and whether this run may save. The macOS build saves with
cache-nix-action/save as an ordinary step, so nothing depends on how
post steps evaluate inputs. Saving needs a push to main as well as
save-nix-cache, matching the sticky disks: a pull request would only
spend ~40 s per leg writing an entry main cannot read, adding eviction
pressure on Blacksmith's cache.

Before saving, the build roots what the next run needs in the store's
own gcroots, like the Linux build: the Crane cacheRoot (keep-outputs
holds its toolchain and vendored sources) and the flake inputs this
build fetched, found with `nix flake archive --dry-run` so unused pins
are not downloaded. The save then collects every other path
(gc-max-store-size-macos: 0), so an entry is one build closure instead
of accumulating every commit's layers and binaries.
rust-toolchain.toml named no profile, so rustup and rust-overlay used
"default", which adds rust-docs: 728 MB and 65,585 files in
rust-docs-1.97.1-aarch64-apple-darwin, roughly half the files of the
macOS build closure. Every cold build downloads and unpacks it (the
v20.0.35 release built rust-docs-1.97.1-aarch64-apple-darwin.tar.xz),
and every cache-nix-action entry tars it, a large share of the work in the ~47 s
gtar extraction on a macOS restore.

Nothing builds or reads the docs. The minimal profile keeps rustc,
cargo, and rust-std, and the existing components list still adds
clippy, llvm-tools-preview, and rustfmt; rust-overlay also stops
pulling libsecret into the Linux toolchain for non-minimal profiles.
The toolchain derivation changes, so every Nix store and the Windows
cargo cache rebuild once.
Release starts before CI saves the entry for the release commit, so it restores the previous main commit's store, which still matches because release commits only bump package.json files.
@ryoppippi
ryoppippi force-pushed the ci/fix-macos-nix-cache branch from 6602f62 to ad91367 Compare October 18, 2026 15:25
@ryoppippi
ryoppippi added this pull request to stack #1926 October 18, 2026 15:25
@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: ad91367a663b
Base SHA: 6548615e30b4

Performance comparison skipped.

Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided.

Base package: 6548615e30b4

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: ad91367a663b
Base SHA: 6548615e30b4

Performance comparison skipped.

Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided.

Base package: 6548615e30b4

@ryoppippi
ryoppippi marked this pull request as ready for review October 18, 2026 17:03
@ryoppippi
ryoppippi merged commit 117ae68 into main Oct 18, 2026
43 of 44 checks passed
@ryoppippi
ryoppippi deleted the ci/fix-macos-nix-cache branch October 18, 2026 17:03

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Review complete — no blocking issues found. Left unapproved per repo policy (reviews do not approve).

Reviewed changes — reviewed the full diff for PR #1921 (macOS Nix cache save fix plus minimal rustup profile).

  • Drop the workflow name from the Nix cache key — setup-nix-cache now keys stores as nix-<job>-<scope>-<deps>-<sha> with newest-first restore prefixes, so release macOS legs restore the entry CI saved on main.
  • Save the macOS store explicitly after rooting its closure — new defer-save input plus primary-key/save outputs; the macOS builder restores only, roots the Crane cacheRoot and fetched flake inputs under /nix/var/nix/gcroots/ci, then saves with gc-max-store-size-macos: '0' so each entry holds one build closure.
  • Minimal rustup profile — rust-toolchain.toml sets profile = "minimal", dropping rust-docs while keeping clippy, llvm-tools-preview, and rustfmt explicit.

Correctness, regression, and release-risk spot checks all passed: the defer-save split matches the documented nested-composite save passthrough failure; both packages.<system>.ccusage[. -darwin-x64].passthru.workspaceArtifacts.cacheRoot attribute paths exist; rooting runs on PRs (validating paths, and the PR run logged Rooted 8 flake inputs) while saving stays gated on main pushes; key sharing between CI and release is content-addressed and matches the existing sticky-disk trust model; the clippy failure in the PR run is pre-existing (#1905, also failing on main). No security or release-blocking concerns. One cold restore per cached job on the first main push after merge, as noted in the PR.

Pullfrog  | Fix it ➔ | View workflow run | Using muse-spark-1.3-contributor (free via Pullfrog for OSS) | 𝕏

Buster210 added a commit to Buster210/ccusage that referenced this pull request Oct 20, 2026
Upstream tip: c76874e (2026-10-20, chore(pricing): update models.dev snapshot)
Previous fork base: 0c58b88 (the upstream commit this fork was synced to)

Synced 83 upstream commits on top of 0c58b88: 56 automated pricing
snapshot refreshes and the following meaningful ones:

  1cd29f5 chore(deps): update pnpm to v12.13.0 (ccusage#1907)
  e4f63ba chore(deps): update node.js to >=24.22.0 (ccusage#1908)
  130e502 ci: regenerate bun.nix automatically (ccusage#1909)
  259ec33 feat(adapter): report stored cost in JSON model breakdowns (ccusage#1903)
  c1eb448 chore: release v20.0.35 (ccusage#1901)
  2a0875f ci(pricing): update snapshots on push to main and daily instead of hourly (ccusage#1914)
  b090428 chore(deps): update github/codeql-action action to v4.39.0 (ccusage#1912)
  4dcf7f5 fix(nix): pin models-dev-gen remeda to models.dev's version and stop Renovate drift (ccusage#1915)
  46edb28 fix(nix): stop the dev shell from copying the whole repository (ccusage#1917)
  58ec8f1 fix(nix): let commits succeed in worktrees without a loaded dev shell (ccusage#1920)
  a570a6f fix(nix): vendor Cargo deps without import-from-derivation (ccusage#1918)
  f6d28c5 fix(antigravity): map model IDs 1418/1419 to gemini-4-argon (ccusage#1905)
  ce439e5 fix(antigravity): map model ID 1050 to gemini-3.1-flash-lite (ccusage#1902)
  e62a66f chore(deps): update pnpm to v12.14.0 (ccusage#1923)
  117ae68 ci(nix): make the macOS Nix cache save a trimmed store with the Crane layers (ccusage#1921)
  7a063a0 ci: shorten the Windows native build critical path (ccusage#1924)
  85ebca3 perf(rust): move pricing snapshots into a leaf crate (ccusage#1922)
  8fe3f47 perf(nix): build Crane layers with the binary's feature resolution (ccusage#1925)
  ac289aa perf(pricing): give the pricing refresh a verify-only TLS 1.3 rustls provider (ccusage#1927)
  d097f74 chore(deps): update rust crate rustls to 0.23.46 (ccusage#1928)
  f8aaf89 docs: use the compact pkg.pr.new URL for bunx previews [ci skip] (ccusage#1929)
  f56bb3d chore: release v20.0.36 (ccusage#1916)
  aaafb41 chore(deps): update dependency vitepress-plugin-group-icons to ^1.8.1 (ccusage#1931)
  0861e7f fix(claude): count requestless resp_ content blocks once (ccusage#1935)
  867c9f3 chore: release v20.0.37 (ccusage#1933)
  fa40bfd chore(deps): update songmu/tagpr action to v1.21.3 (ccusage#1937)
  de99d15 chore(deps): update rust crate zstd to 0.14.3 (ccusage#1936)

Fork wins kept: the SQLite on-disk cache with --live-only and the per-namespace
ledger, the pi adapter and its per-path namespaces, the OpenCode V2 loader, the
FxHash/Cow hot paths, the table renderer, clear-cache, and the tag-only release
workflow. Upstream's own release and tagpr machinery stays out.

Hand-resolved conflicts:

  rust/adapters/claude/src/lib.rs: upstream's resp_ message-id dedupe (ccusage#1935) lands
    beside the fork's requestless replay and collapse_replays rules.
  rust/adapters/claude/src/README.md: upstream's wording for the msg_/resp_ exception.
  rust/Cargo.toml: zstd 0.14.3, keeping the fork's exact `=` pin style.
  .github/workflows/release.yaml: the fork's tag-only release, without upstream's
    tagpr job or the songmu/tagpr action bump.
  apps/ccusage/package.json, docs/package.json, package.json: keep 20.0.34;
    upstream's 20.0.37 bump is dropped.
  packages/ccusage-*/package.json (6 files): stay deleted; the fork ships no npm
    wrapper packages and the release workflow does not read them.
  rust/adapters/claude/src/daily.rs: stays deleted; the fork's daily path lives in
    lib.rs and shares the dedupe helper the fix changes.

Fixes made while landing this sync, folded in so the merge stands green on its own:

  fix(ci): save the Nix store from the native build composites. The nested
    cache-nix-action read an empty primary-key at post-job and every native
    build failed; upstream's defer-save pattern is the fix.
  fix(rust): keep the agent-recorded cost for deleted sources. The recorded_cost
    ledger column carries it, so storedCost survives a deleted file. The ledger
    schema version stays at 2: the column is not a LedgerEntry field, so the blob
    layout is unchanged and the previously released binary keeps working.
  fix(rust): make the file fingerprint helper private, the one hawk finding. It is
    inherited from the pre-sync master, not introduced here.

Keep 20.0.34 numbering; DO NOT push a v20.0.34 tag from this fork (release.yaml
would publish fork bytes under upstream identity).
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