lzip-parallel 0.2.0

Pure Rust parallel ZIP decompressor — multi-core DEFLATE decode for .zip archives
Documentation
//! Full ZIP decompression benchmark with wall-clock timing and core utilization.
//!
//! Downloads a real ZIP (JAR) from Maven Central (cached), then benchmarks:
//! - Full parallel decompress (all cores)
//! - Reports wall time, throughput, and effective core utilization.
//!
//! Run: cargo bench --bench zip_throughput

use criterion::{criterion_group, criterion_main, Criterion, Throughput, BenchmarkId};
use std::fs;
use std::path::PathBuf;

/// Where to cache downloaded test ZIPs.
fn zip_cache_dir() -> PathBuf {
    if let Ok(d) = std::env::var("LZIP_TEST_ZIPS") {
        return PathBuf::from(d);
    }
    let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
    PathBuf::from(home).join("work/test-zips")
}

const MAVEN_BASE: &str = "https://repo1.maven.org/maven2";

struct BenchJar {
    filename: &'static str,
    maven_path: &'static str,
    label: &'static str,
}

const BENCH_JARS: &[BenchJar] = &[
    BenchJar {
        filename: "guava-33.2.1-jre.jar",
        maven_path: "com/google/guava/guava/33.2.1-jre/guava-33.2.1-jre.jar",
        label: "guava_3.4MB",
    },
    BenchJar {
        filename: "ecj-3.37.0.jar",
        maven_path: "org/eclipse/jdt/ecj/3.37.0/ecj-3.37.0.jar",
        label: "ecj_4.8MB",
    },
    BenchJar {
        filename: "scala-library-2.13.14.jar",
        maven_path: "org/scala-lang/scala-library/2.13.14/scala-library-2.13.14.jar",
        label: "scala_5.8MB",
    },
    BenchJar {
        filename: "kotlin-stdlib-2.0.0.jar",
        maven_path: "org/jetbrains/kotlin/kotlin-stdlib/2.0.0/kotlin-stdlib-2.0.0.jar",
        label: "kotlin_1.8MB",
    },
];

fn ensure_jar(jar: &BenchJar) -> Option<PathBuf> {
    let dir = zip_cache_dir();
    fs::create_dir_all(&dir).ok()?;
    let path = dir.join(jar.filename);
    if path.exists() {
        return Some(path);
    }
    let url = format!("{}/{}", MAVEN_BASE, jar.maven_path);
    eprintln!("  downloading {} ...", jar.filename);
    let output = std::process::Command::new("curl")
        .args(["-sSfL", "-o"])
        .arg(&path)
        .arg(&url)
        .output()
        .ok()?;
    if output.status.success() {
        Some(path)
    } else {
        None
    }
}

fn bench_zip_decompress(c: &mut Criterion) {
    let mut group = c.benchmark_group("zip_decompress");
    group.sample_size(20); // fewer iterations since these are larger

    for jar in BENCH_JARS {
        let path = match ensure_jar(jar) {
            Some(p) => p,
            None => {
                eprintln!("  skipping {} (download failed)", jar.filename);
                continue;
            }
        };

        let data = fs::read(&path).unwrap();
        let file_size = data.len() as u64;
        group.throughput(Throughput::Bytes(file_size));

        group.bench_with_input(
            BenchmarkId::new("lzip", jar.label),
            &data,
            |b, data| {
                b.iter(|| {
                    let entries = lzip::decompress_zip(data).unwrap();
                    criterion::black_box(entries.len())
                })
            },
        );
    }

    group.finish();
}

fn bench_lzip_vs_zip_crate(c: &mut Criterion) {
    let mut group = c.benchmark_group("jar_vs_zip_crate");
    group.sample_size(20);

    // Pick one representative JAR for the head-to-head
    let jar = &BENCH_JARS[0]; // guava
    let path = match ensure_jar(jar) {
        Some(p) => p,
        None => {
            eprintln!("  skipping comparison (download failed)");
            return;
        }
    };

    let data = fs::read(&path).unwrap();
    let file_size = data.len() as u64;
    group.throughput(Throughput::Bytes(file_size));

    group.bench_function("lzip", |b| {
        b.iter(|| {
            let entries = lzip::decompress_zip(&data).unwrap();
            criterion::black_box(entries.len())
        })
    });

    group.bench_function("zip_crate_sequential", |b| {
        b.iter(|| {
            use std::io::{Cursor, Read};
            let cursor = Cursor::new(&data);
            let mut archive = zip::ZipArchive::new(cursor).unwrap();
            let mut total = 0usize;
            for i in 0..archive.len() {
                let mut entry = archive.by_index(i).unwrap();
                if entry.is_dir() { continue; }
                let mut buf = Vec::with_capacity(entry.size() as usize);
                entry.read_to_end(&mut buf).unwrap();
                total += buf.len();
            }
            criterion::black_box(total)
        })
    });

    group.finish();
}

fn bench_core_scaling(_c: &mut Criterion) {
    // Core scaling requires re-creating the thread pool per iteration.
    // The OnceLock pool design makes this impractical in a benchmark.
    // Use `time lzip <file>` with LZIP_THREADS=N for manual scaling tests.
}

criterion_group!(benches, bench_zip_decompress, bench_lzip_vs_zip_crate);
criterion_main!(benches);