Skip to content

tar follow-ups: snapshots on the tar engine, reads that end with their data - #1908

Merged
zechengz merged 4 commits into
mainfrom
claude/tar-followups
Oct 27, 2026
Merged

zechengz merged 4 commits into
mainfrom
claude/tar-followups

Conversation

@bytecii

@bytecii bytecii commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR does two things on both hosts:

  • it moves snapshot tar_io onto mirage-rs's tar engine;
  • it lets a gathered Read carry end-of-input with its last bytes.

The git 2.56 work that started here landed separately as #1909; this branch now takes main's version of every git file, so none of that is in this diff.

1. Snapshots on the tar engine

  • write_tar/read_tar (Python) and writeSnapshotTar/readSnapshotTar/writeTar/readTar (TypeScript) now drive the tar stream with a small host:
    • Writing: the manifest goes first, then each blob. A disk file is opened only while it is read, so a large snapshot no longer holds every file open at once.
    • Reading: one pass, with the compressor sniffed. The manifest comes first; each disk mount's blob is staged under staging as it arrives, and every other blob is held in memory.
  • tarfile is gone from the snapshot path and the server's load route. modern-tar is gone from core's dependencies, the lockfile and the tests:
    • the snapshot tests build their foreign archives by hand;
    • the relay test makes its .tgz with mirage's own tar -cz.
  • Compatibility:
    • Snapshots written by tarfile (pax, plain, .gz, .xz) still load.
    • New snapshots are GNU format with ././@LongLink for long names, and read back fine in tarfile.
  • The safety checks hold:
    • a captured file replaced by a link is refused before anything is written;
    • a staged blob cannot escape staging;
    • unsafe or missing blob paths raise the same errors as before.

2. A gathered Read ends with its data

  • StreamHost (both hosts) now sends end together with the last bytes of a least read that reaches end-of-input. Patch's host already did.

  • tar's and zip's readers use those bytes and then stop. Before, each member cost one more round trip to learn the input had ended.

  • The Rust test host does the same, so tar's and zip's unit tests exercise it.

  • Measured on a disk mount, eight runs each:

    Case TypeScript CPU Python CPU
    tar -c of 1,000 × 100 KiB files 1.026 → 1.002 s 0.885 → 0.877 s
    tar -c of one 200 MiB file unchanged unchanged
    tar -c of the whole 298 MiB tree 2.53 → 2.50 s 1.76 → 1.75 s

    The gain is small (1–2%) and about the size of the noise at a load average around 6. The extra round trip was cheaper than tar: one Rust engine for both hosts #1893 estimated.

Size

+608 / −316 lines over 21 files: snapshots are most of it, the read change +66 / −30. Driving the engine needs a small host per side, which costs more lines than calling tarfile and modern-tar did; in exchange modern-tar leaves the npm tree.

Verification

  • Rust:
    • cargo fmt --check, cargo clippy --workspace --all-targets -D warnings (with PYO3_PYTHON set to the venv), and -p mirage-rs-wasm --target wasm32-unknown-unknown: clean.
    • cargo test --workspace: all pass, 592 in mirage-rs.
  • Python:
    • uv run pytest -n 8 before the latest merge: 33,972 passed, 1 failed. The failure, test_cancelling_a_call_kills_what_the_child_started, is a timing-sensitive sandbox test; it passed 3 out of 3 alone.
    • After merging main: the workspace, git, stream-host, generic-command and server suites, 12,614 passed.
  • TypeScript:
    • Core vitest before the merge: 24,119 passed. The 47 failures are the QuickJS-backed js and runtime tests, which exit 127 because qjs-wasi isn't installed locally; it's the same set as on main.
    • After merging main: snapshot, relay, stream-host, git, tar and zip tests, 3,229 passed. tsc: clean.
  • Integ before the merge: ram, disk, git-ram and git-disk, 18,412 passed per host, 0 failed.
  • Gates:
    • check_symbol_parity --strict: baseline lowered from main's 2,913 to 2,910.
    • check_layout_parity --strict, check_barrel_surface, check_case_targets --strict (27), check_spec_parity (101 commands, 12 CLI trees), check_cli_cold_start, check_release_artifacts and check_dependency_licenses --python --npm (917 packages): pass.
    • pre-commit on every changed file: clean.

🤖 Generated with Claude Code

@bytecii
bytecii requested a review from zechengz as a code owner October 27, 2026 19:05
@greptile-apps

greptile-apps Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

Moves snapshot reads and writes onto the shared Rust tar engine. Gathered reads can now carry their last bytes alongside end.

  • Workspace snapshots now use Mirage’s tar engine on both hosts.
  • Gathered reads return their last bytes with the end marker.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Save["Manifest and blobs"] --> Host["Python or TypeScript snapshot host"]
  Host <--> Engine["Rust tar engine"]
  Host <--> Archive["Plain or compressed archive"]
  Host --> Stage["Referenced disk blobs staged"]
  Host --> Held["Other referenced blobs held in memory"]
  Host --> Skip["Unused members discarded"]
Loading

Reviews (6) · Last reviewed commit: "snapshot tars: refuse blob paths that na..." · Reviewed by Greptile

Comment thread python/mirage/workspace/snapshot/tar_io.py Outdated
Comment thread typescript/packages/core/src/workspace/snapshot/tar_io.ts Outdated
Comment thread python/mirage/commands/cli/builtin/git/util.py
Comment thread python/mirage/workspace/snapshot/tar_io.py Outdated
Comment thread python/mirage/commands/cli/builtin/git/util.py Outdated
zechengz added a commit that referenced this pull request Oct 27, 2026
log --graph draws a fork the way 2.56 does, -h exits 0 with its usage
on stdout, an ambiguous abbreviation prints the usage on stderr, and the
long option tables follow 2.56's --help-all. Split out of #1908 so main
goes green without the tar work.
@bytecii bytecii changed the title tar follow-ups and git 2.56: snapshots on the tar engine, reads that end with their data tar follow-ups: snapshots on the tar engine, reads that end with their data Oct 27, 2026
Comment thread python/mirage/workspace/snapshot/tar_io.py
Comment thread python/mirage/workspace/snapshot/tar_io.py Outdated
zechengz and others added 4 commits October 27, 2026 16:34
…end with their data, mirage's git as CI's git 2.56

Snapshots (both hosts) read and write their tar through mirage-rs's tar
instead of tarfile and modern-tar: the manifest first, each blob after it,
a disk mount's file staged as it arrives, old pax and compressed snapshots
read as before. modern-tar leaves the npm tree.

A gathered Read that reaches the end of its input sends its last bytes with
end, so tar -c and zip ask once less per member.

CI's runner moved to git 2.56.0: -h and --help exit 0, log --graph draws a
root apart from the commit below it, an ambiguous option's usage goes to
stderr, and git branch has --forked, --delete-merged and --dry-run. Mirage
follows on both hosts, with integ cases. The native lists keep only forms
git 2.54 to 2.56 answer alike (the read-only fixture's shared skew commit
now has a parent); the 2.56-only graph forms are pinned next to them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opy; keep a header an unset key shares

read_tar and readTar read past a member the manifest names no blob for
instead of holding it, so a small compressed upload cannot expand into
memory. A staged file's writer is aborted when its copy into the file
fails, so a restore reports the failure rather than waiting on it; writeTar
shares that writer. Taking an upstream's keys out of a branch section keeps
a header that one of them shared a line with, as git 2.56 does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…perands alone

A blob path such as ./b//f is written as given, but tar's -x names the
member b/f: both hosts now key held and staged blobs by that member name and
look a reference up the same way, so such a snapshot reads back. rev-list,
rev-parse and diff-tree take -h as help only when it is the verb's one
operand and no option of the verb's came with it, instead of when the line
ends in the verb's name and -h; the integ case pins a branch named after the
verb and an option beside -h, as git 2.56 refuses both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blob named elsewhere too

Two blob paths that tar's -x gives one member name (b/f and ./b//f) are
refused when the snapshot is written, and a member stored twice is refused
when it is read, so distinct blobs never come back as the same bytes. A disk
file staged on restore that the manifest also names elsewhere is held in
memory as well, so that other reference still resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zechengz
zechengz force-pushed the claude/tar-followups branch from ca8022a to 12eeca9 Compare October 27, 2026 23:34
@zechengz
zechengz merged commit c707140 into main Oct 27, 2026
69 checks passed
@zechengz
zechengz deleted the claude/tar-followups branch October 28, 2026 00:00
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.

2 participants