Derive zip member offsets from the central directory - #1080
Merged
TomNicholas merged 2 commits intoAug 6, 2026
Conversation
parse_zip_index read every member's 30-byte local file header to find where its data starts, because ZIP permits the local extra field to differ in length from the central directory's copy. That is one request per member - 337 for a 336-member archive - and on remote object storage it dominated the cost of virtualizing an archive. Presume the two agree, and verify it for free instead: in an archive written straight through, each member's data ends exactly where the next member's local header begins, and the last ends at the central directory, so offsets short by a stray extra field would leave a hole. Archives that fail the check - zipfile's force_zip64, Info-ZIP's zip, or anything with gaps - fall back to reading the local headers exactly as before. Measured against a 336-member archive on remote object storage: 337 range requests to 1, and open_virtual_dataset from ~7.8s to ~1.3s. Because virtualizing many archives at once turns out to be request-rate bound rather than latency bound, batch throughput improved 6.8x.
TomNicholas
had a problem deploying
to
test-release
August 6, 2026 20:27 — with
GitHub Actions
Error
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.
parse_zip_indexreads every member's 30-byte local file header to find where its data starts, because ZIP permits a local header's extra field to differ in length from the central directory's copy of it. That costs one request per member — 337 for a 336-member archive — and on remote object storage it dominates the cost of virtualizing an archive.Presume the two agree, and verify that presumption for free. In an archive written straight through, each member's data ends exactly where the next member's local header begins, and the last member's ends at the central directory. Offsets short by a stray extra field would leave a hole, so an archive that satisfies this for every member cannot be fooled by one. Archives that fail the check fall back to reading the local headers, exactly as before.
The assumption is not safe on its own, which is why it's checked rather than assumed. Measured across archives:
zarr.storage.ZipStore, V2 / V3 / large members / streamedzipfilewithforce_zip64=Truezip -0No case was found where the check passes but the offsets are wrong.
zip -0matters here because the parser's ownNotImplementedErrorfor compressed archives tells users to rebuild with it, so non-ZipStorearchives are explicitly in scope.Results
Against a 336-member, 171 MB archive on remote object storage:
parse_zip_indexrange requestsparse_zip_indexwall timeopen_virtual_dataset, one archiveThe throughput number is the interesting one. Virtualizing many archives at once turned out to be request-rate bound, not latency bound — adding threads (8/16/32 identical, 64 worse) or processes (1/2/4 identical) did nothing. So eliminating requests moved the ceiling where parallelising them could not.
Tests
test_index_does_not_read_local_headers— a 30-array archive's index costs ≤3 requests, and every member's bytes still matchzipfile.read.test_falls_back_to_local_headers_when_offsets_disagree—force_zip64archives read correctly, and demonstrably via the fallback rather than by luck.Both written before the change and failing (155 and 93 requests). Full suite: 628 passed, no new mypy errors.