Repository: denoland/deno
Pull request: perf(npm): use libdeflater and preallocate buffer

For install with deno.lock and no node_modules/global cache using https://jsr.io/@david/bench-registry:

```
% cd dev/scratch3
scratch3 % ./bench.sh
Benchmark 1: ../deno/target/release/deno_main install --env-file
  Time (mean ± σ):      4.071 s ±  0.212 s    [User: 1.200 s, System: 5.902 s]
  Range (min … max):    3.834 s …  4.456 s    10 runs
 
Benchmark 2: ../deno/target/release/deno_new install --env-file
  Time (mean ± σ):      3.812 s ±  0.221 s    [User: 0.707 s, System: 5.688 s]
  Range (min … max):    3.519 s …  4.279 s    10 runs
 
Summary
  ../deno/target/release/deno_new install --env-file ran
    1.07 ± 0.08 times faster than ../deno/target/release/deno_main install --env-file
```

Changed files:
filename: Cargo.lock
patch: @@ -2782,6 +2782,7 @@ dependencies = [
  "faster-hex",
  "flate2",
  "futures",
+ "libdeflater",
  "log",
  "parking_lot",
  "percent-encoding",
@@ -6236,6 +6237,24 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "libdeflate-sys"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "libdeflater"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71"
+dependencies = [
+ "libdeflate-sys",
+]
+
 [[package]]
 name = "libffi"
 version = "5.1.0"

filename: Cargo.toml
patch: @@ -241,6 +241,7 @@ jupyter-protocol = "=1.2.1"
 jupyter_runtime = { package = "runtimelib", version = "=1.3.0", default-features = false, features = ["tokio-runtime", "aws-lc-rs"] }
 lazy-regex = "3"
 libc = "0.2.168"
+libdeflater = "1.25.2"
 libz-sys = { version = "1.1.20", default-features = false }
 log = { version = "0.4.28", features = ["kv"] }
 lsp-types = { package = "ls-types", version = "=0.0.3" }

filename: libs/npm_cache/Cargo.toml
patch: @@ -40,6 +40,7 @@ url.workspace = true
 deno_error = { workspace = true, features = ["serde", "serde_json", "tokio"] }
 deno_unsync = { workspace = true, features = ["tokio"] }
 aws-lc-rs.workspace = true
+libdeflater.workspace = true
 tokio = { workspace = true, features = ["sync"] }
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]

filename: libs/npm_cache/tarball_extract.rs
patch: @@ -84,14 +84,67 @@ pub fn verify_and_decompress_tarball(
   dist_info: &NpmPackageVersionDistInfo,
 ) -> Result<Vec<u8>, VerifyAndExtractTarballError> {
   verify_tarball_integrity(package_nv, data, &dist_info.integrity())?;
+  decompress_gzip(data)
+}
+
+/// Uses libdeflater for faster gzip decompression with preallocated buffers.
+#[cfg(not(target_arch = "wasm32"))]
+fn decompress_gzip(
+  data: &[u8],
+) -> Result<Vec<u8>, VerifyAndExtractTarballError> {
+  use libdeflater::Decompressor;
+
+  let mut decompressor = Decompressor::new();
+  let size_hint = gzip_decompressed_size_hint(data);
+  let mut decompressed = Vec::new();
+  if decompressed.try_reserve(size_hint).is_err() {
+    return decompress_gzip_streaming(data);
+  }
+  decompressed.resize(size_hint, 0);
+  match decompressor.gzip_decompress(data, &mut decompressed) {
+    Ok(actual_size) => {
+      decompressed.truncate(actual_size);
+      Ok(decompressed)
+    }
+    Err(_) => {
+      // Bad data or insufficient space — fall back
+      // to flate2 for a proper io::Error.
+      decompress_gzip_streaming(data)
+    }
+  }
+}
+
+#[cfg(target_arch = "wasm32")]
+fn decompress_gzip(
+  data: &[u8],
+) -> Result<Vec<u8>, VerifyAndExtractTarballError> {
+  decompress_gzip_streaming(data)
+}
+
+fn decompress_gzip_streaming(
+  data: &[u8],
+) -> Result<Vec<u8>, VerifyAndExtractTarballError> {
   let mut decoder = GzDecoder::new(data);
   let mut decompressed = Vec::new();
+  let _ = decompressed.try_reserve(gzip_decompressed_size_hint(data));
   decoder
     .read_to_end(&mut decompressed)
     .map_err(ExtractTarballError::from)?;
   Ok(decompressed)
 }
 
+/// Read the uncompressed size hint from the gzip trailer (last 4 bytes).
+/// This is the original size mod 2^32, so it's exact for data < 4GB
+/// (which covers virtually all npm packages).
+fn gzip_decompressed_size_hint(data: &[u8]) -> usize {
+  if data.len() >= 4 {
+    let last4 = &data[data.len() - 4..];
+    u32::from_le_bytes([last4[0], last4[1], last4[2], last4[3]]) as usize
+  } else {
+    data.len().saturating_mul(4)
+  }
+}
+
 /// Writes already-decompressed raw tar bytes to disk.
 /// This is I/O-bound work that benefits from limited concurrency
 /// to avoid filesystem contention.