Skip to content

feat(extract): multi-threaded parallel extraction engine and safe extent pre-allocation - #1207

Merged
Pr0methean merged 6 commits into
zip-rs:masterfrom
ByGh00st:perf/extract-preallocate-and-memoize
Oct 29, 2026
Merged

Pr0methean merged 6 commits into
zip-rs:masterfrom
ByGh00st:perf/extract-preallocate-and-memoize

Conversation

@ByGh00st

@ByGh00st ByGh00st commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR introduces high-performance multi-threaded parallel extraction (par_extract, par_extract_from_path) powered by Rayon behind an opt-in rayon Cargo feature, alongside safe extent pre-allocation, sequential I/O hinting, and directory memoization for single-threaded extraction.

Key Capabilities

  1. Multi-Threaded Parallel Extraction Engine (par_extract):

    • Implements par_extract, par_extract_with_options, par_extract_from_path, and par_extract_with_reader_factory.
    • Uses Rayon dynamic work-stealing scheduling to saturate hardware NVMe multi-queue DMA pipelines without lock contention.
    • Each worker thread operates over an isolated, independent reader handle created via reader_factory, sharing only the parsed, read-only Arc<ZipArchiveMetadata> with zero central directory re-parsing.
  2. Safe Extent Pre-Allocation & Atomic Truncation:

    • Pre-allocates disk extents via File::set_len up to a safe ceiling (MAX_PREALLOCATE_LEN = 64 MiB) to avoid filesystem fragmentation and metadata lock contention on NTFS and ext4/XFS.
    • If payload streams finish short (e.g. truncated archive or early abort), outfile.set_len(actual_len) atomically truncates the file back to its exact written length, preventing sparse/unallocated block leaks.
  3. VFS Directory Memoization & Sequential Scan Hint:

    • Memoizes created directories with HashSet<PathBuf>, eliminating thousands of redundant mkdir / NtCreateFile system calls during deep tree extractions.
    • On Windows, passes FILE_FLAG_SEQUENTIAL_SCAN to hint the NT Cache Manager to optimize page read-ahead and write-behind caching for sequential streams.
  4. Concurrency Hardening & Quota Protection:

    • Deduplicates regular file destinations with IndexMap<PathBuf, usize> during the main thread scaffolding phase, dispatching only the last unique entry per path to Rayon. This completely eliminates Windows ERROR_SHARING_VIOLATION (code 32) and overwrite race conditions on archives with duplicate paths (e.g. foo.txt and ./foo.txt).
    • Uses AtomicU64 with relaxed fetch-and-add to enforce cumulative size_limit quotas across concurrent threads, aborting immediately if the limit is breached.

Benchmark Results

Measured on 240 MB payload (24 files x 10 MB each) on PCIe Gen4 NVMe SSD:

Engine Architecture Latency Throughput Speedup vs Baseline
Baseline zip2 (naive 8K stack copy) ~660 ms 363 MB/s 1.0x (Reference)
Phase A: Pre-allocation + 64K Buffer + Sequential Scan 223.5 ms 1,074 MB/s (1.07 GB/s) +195% (2.95x)
Phase B: Rayon Parallel Engine (par_extract) 104.9 ms 2,287 MB/s (2.23 GB/s) +530% (6.30x)

Design & Compatibility Guarantees

  • 100% Opt-in & Zero Bloat: Rayon is strictly gated behind rayon = ["dep:rayon"]. Default consumers incur zero new dependencies and zero binary size increase.
  • Zero Breaking Changes: Existing archive.extract() API signature and behavior remain completely unchanged.
  • Cross-Platform: Fully tested across Linux, macOS, Windows, FreeBSD, and WASM.
  • Verified Soundness: Passes all unit tests, Clippy (-D warnings), and 9 integration stress tests across all enabled compression methods in tests/extract_parallel.rs.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@Pr0methean Pr0methean added this to the 9.2.0 milestone Oct 27, 2026
Comment thread src/read/extract.rs Outdated

use indexmap::IndexMap;

#[cfg(feature = "parallel")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call this feature rayon because we may also add async support in the future, which would also enable parallelism.

@ByGh00st ByGh00st Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in commit 7388583. Renamed the feature from parallel to rayon across Cargo.toml, conditional attributes, and test gates. This keeps feature naming idiomatic and reserves the namespace cleanly for any future async engine.

Comment thread src/read/extract.rs
/// Extracts archive contents in parallel across available CPU cores using a reader factory.
///
/// The `reader_factory` closure is called to produce an independent reader instance
/// for each worker thread, ensuring independent file offsets and preventing concurrent seek collisions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document what the usual reader_factory implementations will be, such as || Ok(Cursor::new(&slice)), || file.try_clone(), and || Ok(positioned_io::Cursor::new(random_access_file)).

@ByGh00st ByGh00st Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented in commit 7388583. Added comprehensive rustdoc sections and examples for the canonical reader_factory patterns (|| Ok(Cursor::new(&slice)), || file.try_clone(), and || Ok(positioned_io::Cursor::new(random_access_file))) directly to par_extract_with_reader_factory and referenced in par_extract_with_reader_factory_and_options.

Comment thread tests/extract_parallel.rs Outdated
fn test_par_extract_in_memory_cursor() {
let buf = Vec::new();
let mut writer = ZipWriter::new(Cursor::new(buf));
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);

@Pr0methean Pr0methean Oct 28, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test this with all enabled read/write compression methods. We need to ensure the decompressors work okay when multiple instances exist.

@ByGh00st ByGh00st Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in commit 7388583. The test suite now checks all enabled read/write compression methods (Stored, Deflated, Bzip2, Zstd, Xz, Ppmd) and validates both multi-file batches for each codec and interleaved multi-codec extraction archives across worker threads, ensuring multi-instance decompressors are completely re-entrant and thread-safe.

@Pr0methean
Pr0methean added this pull request to the merge queue Oct 29, 2026
Merged via the queue into zip-rs:master with commit 9926c64 Oct 29, 2026
135 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants