Skip to content

Derive zip member offsets from the central directory - #1080

Merged
TomNicholas merged 2 commits into
zarr-developers:mainfrom
TomNicholas:skip-zip-local-header-reads
Aug 6, 2026
Merged

Derive zip member offsets from the central directory#1080
TomNicholas merged 2 commits into
zarr-developers:mainfrom
TomNicholas:skip-zip-local-header-reads

Conversation

@TomNicholas

Copy link
Copy Markdown
Member

parse_zip_index reads 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:

archive outcome
zarr.storage.ZipStore, V2 / V3 / large members / streamed check passes, offsets correct
336-member archive from object storage check passes, offsets correct
zipfile with force_zip64=True check fails → falls back (local extra field is 20 bytes longer)
Info-ZIP zip -0 check fails → falls back

No case was found where the check passes but the offsets are wrong. zip -0 matters here because the parser's own NotImplementedError for compressed archives tells users to rebuild with it, so non-ZipStore archives are explicitly in scope.

Results

Against a 336-member, 171 MB archive on remote object storage:

before after
parse_zip_index range requests 337 1
parse_zip_index wall time 2.53 s 0.35 s
open_virtual_dataset, one archive 7.8 s 1.34 s
throughput, 18 archives / 32 threads 0.55 files/s 3.76 files/s

The 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 match zipfile.read.
  • test_falls_back_to_local_headers_when_offsets_disagreeforce_zip64 archives 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.

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
TomNicholas merged commit 81b30c4 into zarr-developers:main Aug 6, 2026
1 of 2 checks passed
@TomNicholas
TomNicholas deleted the skip-zip-local-header-reads branch August 6, 2026 20:33
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