perf(cache): switch local cache to <hash>.tar.zst artifacts (Turbo-style) - #86
Merged
Conversation
…yle) After actually reading Turbo's cache_archive code, they use tar archives (NOT hardlinks like my prior PR #85 attempted). Converting local cache to the same layout: <cacheDir>/<hash>.tar.zst ← per-entry artifact outputs/ ← only if task has output files <project-relative>... stdout ← only if non-empty stderr ← only if non-empty <cacheDir>/cache.db ← unchanged: hash/project/task/cmd/ exit_code/duration_ms/size/timestamps Why this should feel "instant" like Turbo: 1. **One sequential disk read** of one compressed file instead of N small file reads scattered across <hash>/ directory trees. The kernel prefetches the whole tar in one pass. 2. **One subprocess invocation** (tar -xf) handles all the file writes in C, instead of N async Bun.write() calls from JS. 3. **Bun.zstdCompress / zstdDecompress** for compression — built in, no external `zstd` binary required. Artifact design per user direction: - Outputs/stdout/stderr ONLY appear if non-empty. No empty folders, no zero-byte files. - Empty task (no outputs, no logs) → ~1 KB empty archive. - DB carries the metadata index (command, exit_code, etc.); artifact stays a pure dump. - Group tasks never touch the cache (executeGroupTask returns early without calling save/get/restore). Implementation: - Drop `<hash>/outputs/`, `<hash>/stdout`, `<hash>/stderr` layout. - `save()`: stage outputs/+stdout+stderr into temp dir, tar to stdout, zstd-compress, atomic rename to `<hash>.tar.zst`. - `get()`: read+decompress tar once; `peekTar()` walks header blocks to grab entries list + stdout/stderr content in one pass. No subprocess. - `restoreOutputs()`: decompress; if `outputs/` member present, `tar -xf - --strip-components=1 outputs` to projectDir. Otherwise skip (the artifact may carry only logs). - SCHEMA_VERSION v13 → v14 (entries table no longer has stdout/ stderr columns). Schema mismatch nukes everything per project convention (pre-alpha; documented in CLAUDE.md). - Removed obsolete `outputsMatchCache` (the per-file manifest skip Turbo does inline during extract is a deferred optimization). - Removed obsolete hardlink-restore code (wrong direction — Turbo explicitly rejects hardlink tar entries). Tests: 458/458 pass. - 6 new tests in `tests/cache-perf.test.ts` for the v15 layout (single file, no <hash>/ dir, stdout/stderr in tar not DB, restore round-trips, no-op when artifact missing, outputsPath returns .tar.zst, prune removes .tar.zst). - Updated 2 cache.test.ts assertions that pinned the old v13 directory layout. Expected perf impact: matches Turbo's "instant" feel on restore — single sequential read + one tar invocation rather than N file copies. Will validate against the user's 100-pkg / 300-task benchmark.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
After reading Turbo's actual
cache_archive/restore.rs: they usetar archives, not hardlinks. Converting our local cache to the
same layout.
Artifact layout
Empty task (no outputs, no logs) → ~1 KB empty archive.
The artifact is a pure dump of run byproducts; nothing else.
DB layout
stdout/stderr removed from the entries row — they live in the
artifact so a remote pull carries them with the bytes.
SCHEMA_VERSION bumped v13 → v14 (mismatch nukes per project
convention; pre-alpha).
Why this should feel like Turbo
N small files scattered across
<hash>/trees. Kernel prefetchesthe whole tar in one pass.
tar -xf) handles all file writes in Cinstead of N async
Bun.write()calls from JS.zstdbinary required.Group tasks never touch the cache
executeGroupTaskreturns early with a hash computed from upstreamoutcomes; never calls
save/get/restoreOutputs. Confirmedthe contract holds.
What got removed
<hash>/outputs/,<hash>/stdout,<hash>/stderrlayout.outputsMatchCachewhole-restore-skip (the per-file manifestskip Turbo does inline during extract is a deferred optimization).
explicitly rejects hardlink tar entries.
Implementation notes
save(): stage outputs/+stdout+stderr in a temp dir, tar tostdout, zstd-compress, atomic rename to
<hash>.tar.zst. Stagecontents are conditional — no folder if no outputs, no file if
no stdout/stderr.
get(): read tar once into memory;peekTar()walks headerblocks directly (no subprocess) to grab the entries list +
stdout/stderr content in one pass.
restoreOutputs(): decompress; if the artifact has anoutputs/member,
tar -xf - --strip-components=1 outputsextracts thefiles into the project dir. Skips when only logs are cached.
Test plan
bun src/bin.ts run ci— 3/3 pass (format-check + lint + 458 tests)tests/cache.test.tsassertions (old<hash>/layout).tests/cache-perf.test.ts:<hash>.tar.zstfile (no<hash>/dir)outputsPathreturns.tar.zstprunedeletes.tar.zstExpected impact
Should match Turbo's "instant" feel — single sequential read +
one tar invocation rather than N copies. Will validate against
your 100-pkg / 300-task benchmark.
https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9
Generated by Claude Code