Skip to content

fix(cache): leave outputs that already match untouched when restoring a cache hit - #901

Open
jasonkuhrt wants to merge 3 commits into
voidzero-dev:mainfrom
jasonkuhrt:fix/cache-restore-skip-unchanged
Open

jasonkuhrt wants to merge 3 commits into
voidzero-dev:mainfrom
jasonkuhrt:fix/cache-restore-skip-unchanged

Conversation

@jasonkuhrt

@jasonkuhrt jasonkuhrt commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Written by an AI agent (Claude Opus 5.5) on behalf of @jasonkuhrt.

Closes #900

Motivation

A cache hit unpacked every archived output over the workspace, so each file was removed and created again even when its content hadn't changed, and a watcher on the outputs saw all of them change. In #900, a replay of a 1,000-file generated client made a Vite dev server send about 1,000 HMR updates and restarted an API running under vite build --watch.

Change

extract_output_archive compares each regular-file entry with the file on disk before writing it:

  • A file with the entry's size, permissions and bytes is left as it is, modification time included.
  • A missing file, or one that differs in type, size or permissions, is unpacked right away with Entry::unpack_in, as Archive::unpack did, so tar's path checks and the metadata it restores stay the same. The destination is canonicalized once, as Archive::unpack does.
  • A file of the same size is compared by streaming the entry and the file side by side, which reads the entry's data. One that differs is unpacked in a second pass over the archive, which stops after the last such entry. A hit over unchanged outputs, or a restore into a fresh checkout, reads the archive once.

Only entries whose path is plain relative components are compared; anything else goes to unpack_in as before. A match writes nothing, so tar's check that a write stays inside the workspace still guards every write.

On the fixture in #900, a replay over unchanged outputs replaces none of the 1,000 files and fires no watcher events, against all 1,000 on main. With one output edited and one deleted, it restores those 2. On Linux the same replay fired 3,000 inotify events on main and none with the change.

Release builds on GitHub's macos-15 and ubuntu-24.04 runners, each job building both revisions and alternating their runs, median of 40 (run):

Cache hit, 1,000 outputs macOS, main macOS, this PR Linux, main Linux, this PR
Outputs unchanged 311.3 ms 35.4 ms 50.5 ms 13.8 ms
One output edited, same size 244.9 ms 27.3 ms 50.9 ms 14.1 ms
Outputs deleted 200.8 ms 215.4 ms 39.2 ms 41.2 ms

A restore into an empty directory is 5–7% slower, from looking up each missing file before unpacking it.

Two new tests fail on main: matching files keep their modification time, and missing, resized and same-size-changed files are restored beside an untouched one. A third checks that a file whose permissions changed is restored.

#866 (atomic writes) and #885 (symbolic links) also change this function. This change only decides which entries get written, so it combines with either.

jasonkuhrt added a commit to jasonkuhrt/vite-task that referenced this pull request Oct 28, 2026
jasonkuhrt and others added 3 commits October 28, 2026 16:42
… a cache hit

Restoring a cache hit unpacked every archived output over the workspace, so
tar removed and recreated each file even when its content was unchanged.
A file watcher saw every output change on every hit.

Compare each regular-file entry with the file on disk first: a file with the
same size, permissions and bytes is left as it is. A missing file, or one
that differs in type, size or permissions, is unpacked right away, as before.
One that matches in size but differs in content was found by reading the
entry's data, so it is unpacked in a second pass over the archive.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

A cache hit rewrites every output file, unchanged ones included, so watchers see all outputs change

1 participant