Repository navigation
deps: replace cloudflare/zlib with zlib-ng 2.3.3 #29433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
f839a51
a0f86c5
f10b561
360b598
8bc0ff7
a5e7cea
d1d7fdc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // Benchmark fetch() decoding gzip Content-Encoding — the path PR #16100 | ||
| // found regressed (Next.js standalone server). Server gzips responses, | ||
| // client inflates them via the streaming inflate path. | ||
| import { bench, group, run } from "../runner.mjs"; | ||
| import zlib from "node:zlib"; | ||
|
|
||
| const html = Buffer.from("<div>Hello World</div>".repeat(500)); | ||
| const htmlGz = zlib.gzipSync(html, { level: 6 }); | ||
|
|
||
| using server = Bun.serve({ | ||
| port: 0, | ||
| fetch(req) { | ||
| const url = new URL(req.url); | ||
| if (url.pathname === "/gz") { | ||
| return new Response(htmlGz, { | ||
| headers: { "Content-Encoding": "gzip", "Content-Type": "text/html" }, | ||
| }); | ||
| } | ||
| return new Response(html, { headers: { "Content-Type": "text/html" } }); | ||
| }, | ||
| }); | ||
|
|
||
| const base = `http://localhost:${server.port}`; | ||
|
|
||
| group("fetch + gzip decode", () => { | ||
| bench("11KB gzipped → text()", async () => { | ||
| const res = await fetch(`${base}/gz`); | ||
| await res.text(); | ||
| }); | ||
| bench("11KB plain → text()", async () => { | ||
| const res = await fetch(`${base}/plain`); | ||
| await res.text(); | ||
| }); | ||
| }); | ||
|
|
||
| await run(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| // Comprehensive zlib benchmark: sync, async, streaming, realistic payloads. | ||
| // Designed to compare cloudflare-zlib vs zlib-ng on the workloads Bun cares about: | ||
| // - HTTP gzip response encoding (streaming, level 1/6) | ||
| // - npm tarball gunzip (streaming inflate, ~10-500KB inputs) | ||
| // - fetch() Content-Encoding: gzip decoding | ||
| // - Bun.gzipSync / node:zlib one-shot | ||
|
|
||
| import { bench, group, run } from "../runner.mjs"; | ||
| import zlib from "node:zlib"; | ||
| import { promisify } from "node:util"; | ||
| import { Readable } from "node:stream"; | ||
| import { pipeline } from "node:stream/promises"; | ||
| import crypto from "node:crypto"; | ||
|
|
||
| const gzip = promisify(zlib.gzip); | ||
| const gunzip = promisify(zlib.gunzip); | ||
| const deflate = promisify(zlib.deflate); | ||
| const inflate = promisify(zlib.inflate); | ||
|
|
||
| // ─── Test corpora ─── | ||
| // "html": realistic web page — repeated structure with varying content (compresses well, ~85%) | ||
| // "json": API response — keys repeat, values vary (compresses ~70%) | ||
| // "binary": random bytes — incompressible, exercises the "give up fast" path | ||
| // "code": minified JS — what npm tarballs actually contain | ||
|
|
||
| function makeHtml(kb) { | ||
| let s = "<!DOCTYPE html><html><head><title>Bench</title></head><body>"; | ||
| let i = 0; | ||
| while (s.length < kb * 1024) { | ||
| s += `<div class="item" id="item-${i}"><h2>Item ${i}</h2><p>Lorem ipsum dolor sit amet ${i % 17}.</p></div>`; | ||
| i++; | ||
| } | ||
| return Buffer.from(s); | ||
| } | ||
|
|
||
| function makeJson(kb) { | ||
| const arr = []; | ||
| let i = 0; | ||
| while (JSON.stringify(arr).length < kb * 1024) { | ||
| arr.push({ id: i, name: `user_${i}`, email: `u${i}@example.com`, active: i % 3 === 0, score: (i * 7919) % 1000 }); | ||
| i++; | ||
| } | ||
| return Buffer.from(JSON.stringify(arr)); | ||
| } | ||
|
|
||
| const corpora = { | ||
| "html-4K": makeHtml(4), | ||
| "html-128K": makeHtml(128), | ||
| "html-1M": makeHtml(1024), | ||
| "json-128K": makeJson(128), | ||
| "binary-128K": crypto.randomBytes(128 * 1024), | ||
| }; | ||
|
|
||
| // Pre-compress for decompression benches | ||
| const gzipped = {}; | ||
| for (const [name, buf] of Object.entries(corpora)) { | ||
| gzipped[name] = zlib.gzipSync(buf, { level: 6 }); | ||
| } | ||
|
|
||
| // ─── Sync one-shot ─── | ||
| group("gzipSync level=1", () => { | ||
| for (const [name, buf] of Object.entries(corpora)) { | ||
| bench(name, () => zlib.gzipSync(buf, { level: 1 })); | ||
| } | ||
| }); | ||
|
|
||
| group("gzipSync level=6", () => { | ||
| for (const [name, buf] of Object.entries(corpora)) { | ||
| bench(name, () => zlib.gzipSync(buf, { level: 6 })); | ||
| } | ||
| }); | ||
|
|
||
| group("gunzipSync", () => { | ||
| for (const [name, buf] of Object.entries(gzipped)) { | ||
| bench(name, () => zlib.gunzipSync(buf)); | ||
| } | ||
| }); | ||
|
|
||
| // ─── Async one-shot (threadpool) ─── | ||
| group("gzip async level=6", () => { | ||
| for (const [name, buf] of Object.entries(corpora)) { | ||
| bench(name, async () => await gzip(buf, { level: 6 })); | ||
| } | ||
| }); | ||
|
|
||
| group("gunzip async", () => { | ||
| for (const [name, buf] of Object.entries(gzipped)) { | ||
| bench(name, async () => await gunzip(buf)); | ||
| } | ||
| }); | ||
|
|
||
| // ─── Streaming (HTTP server / npm install path) ─── | ||
| // Feed input in 16KB chunks like a real socket would. | ||
| function chunked(buf, size = 16 * 1024) { | ||
| const chunks = []; | ||
| for (let i = 0; i < buf.length; i += size) chunks.push(buf.subarray(i, i + size)); | ||
| return chunks; | ||
| } | ||
|
|
||
| const streamInputs = { | ||
| "html-128K": chunked(corpora["html-128K"]), | ||
| "html-1M": chunked(corpora["html-1M"]), | ||
| }; | ||
| const streamGzInputs = { | ||
| "html-128K": chunked(gzipped["html-128K"]), | ||
| "html-1M": chunked(gzipped["html-1M"]), | ||
| }; | ||
|
|
||
| async function drain(stream) { | ||
| for await (const _ of stream); | ||
| } | ||
|
|
||
| group("createGzip stream level=1", () => { | ||
| for (const [name, chunks] of Object.entries(streamInputs)) { | ||
| bench(name, async () => { | ||
| const gz = zlib.createGzip({ level: 1 }); | ||
| const src = Readable.from(chunks); | ||
| await pipeline(src, gz, drain); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| group("createGzip stream level=6", () => { | ||
| for (const [name, chunks] of Object.entries(streamInputs)) { | ||
| bench(name, async () => { | ||
| const gz = zlib.createGzip({ level: 6 }); | ||
| const src = Readable.from(chunks); | ||
| await pipeline(src, gz, drain); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| group("createGunzip stream", () => { | ||
| for (const [name, chunks] of Object.entries(streamGzInputs)) { | ||
| bench(name, async () => { | ||
| const gz = zlib.createGunzip(); | ||
| const src = Readable.from(chunks); | ||
| await pipeline(src, gz, drain); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| // ─── deflateInit/inflateInit overhead (small payloads, many iterations) ─── | ||
| // zlib-ng has higher init cost due to larger state structs. This matters for | ||
| // per-request gzip on tiny responses. | ||
| const tiny = Buffer.from("Hello, World!"); | ||
| const tinyGz = zlib.gzipSync(tiny); | ||
|
|
||
| group("init overhead (13B payload)", () => { | ||
| bench("gzipSync", () => zlib.gzipSync(tiny, { level: 6 })); | ||
| bench("gunzipSync", () => zlib.gunzipSync(tinyGz)); | ||
| bench("deflateSync", () => zlib.deflateSync(tiny, { level: 6 })); | ||
| bench("inflateSync", () => zlib.inflateSync(zlib.deflateSync(tiny))); | ||
| }); | ||
|
|
||
| // ─── Compression ratio (printed, not benched) ─── | ||
| console.log("\n# Compression ratio (output bytes, level=6):"); | ||
| console.log("# corpus input output ratio"); | ||
| for (const [name, buf] of Object.entries(corpora)) { | ||
| const out = zlib.gzipSync(buf, { level: 6 }); | ||
| console.log( | ||
| `# ${name.padEnd(12)} ${String(buf.length).padStart(8)} ${String(out.length).padStart(8)} ${((out.length / buf.length) * 100).toFixed(1)}%`, | ||
| ); | ||
| } | ||
| console.log(`# zlib version: ${process.versions.zlib}\n`); | ||
|
Comment on lines
+54
to
+165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a correctness check for the benchmark outputs. These benches discard every result, so truncated or corrupted output still looks like a performance win. The stream case is especially vulnerable because 🤖 Prompt for AI Agents |
||
|
|
||
| await run(); | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| clang-cl on Windows ARM64 defines _MSC_VER but is not real MSVC: it ships | ||
| clang's own <arm_neon.h>/<arm_acle.h>. MSVC's <arm64_neon.h> defines | ||
| vld1q_u16_x4/vld1q_u8_x4/vst1q_u16_x4 as macros, which macro-expand the | ||
| polyfill function definitions at neon_intrins.h:40-63 into garbage. MSVC's | ||
| <intrin.h> lacks the ARM ACLE CRC intrinsics (__crc32b etc.) that clang's | ||
| <arm_acle.h> provides. | ||
|
|
||
| Gate the MSVC-specific includes on !defined(__clang__) so clang-cl takes the | ||
| standard ACLE/NEON path. Also fix the matching cmake feature-detection probes | ||
| so HAVE_ARMV8_INTRIN and NEON_HAS_LD4 succeed under clang-cl, which sets | ||
| ARM_CRC32_INTRIN/ARM_NEON_HASLD4 and skips the polyfills entirely. | ||
|
|
||
| Upstream: not yet reported (zlib-ng CI does not cover clang-cl ARM64). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial File the upstream issue before this patch becomes institutional memory. This fix is now load-bearing for a supported toolchain. Please add an upstream issue/PR reference here once filed so the next zlib-ng bump has a clear breadcrumb for whether the patch was merged, reverted, or superseded. 🤖 Prompt for AI Agents |
||
| --- a/arch/arm/neon_intrins.h | ||
| +++ b/arch/arm/neon_intrins.h | ||
| @@ -1,7 +1,7 @@ | ||
| #ifndef ARM_NEON_INTRINS_H | ||
| #define ARM_NEON_INTRINS_H | ||
|
|
||
| -#if defined(_MSC_VER) && (defined(_M_ARM64) || defined(_M_ARM64EC)) | ||
| +#if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_ARM64) || defined(_M_ARM64EC)) | ||
| /* arm64_neon.h is MSVC specific */ | ||
| # include <arm64_neon.h> | ||
| #else | ||
| --- a/arch/arm/acle_intrins.h | ||
| +++ b/arch/arm/acle_intrins.h | ||
| @@ -2,7 +2,7 @@ | ||
| #define ARM_ACLE_INTRINS_H | ||
|
|
||
| #include <stdint.h> | ||
| -#ifdef _MSC_VER | ||
| +#if defined(_MSC_VER) && !defined(__clang__) | ||
| # include <intrin.h> | ||
| #elif defined(HAVE_ARM_ACLE_H) | ||
| # include <arm_acle.h> | ||
| --- a/cmake/detect-intrinsics.cmake | ||
| +++ b/cmake/detect-intrinsics.cmake | ||
| @@ -42,7 +42,7 @@ macro(check_armv8_compiler_flag) | ||
| ) | ||
| # Check whether compiler supports ARMv8 intrinsics | ||
| check_c_source_compiles( | ||
| - "#if defined(_MSC_VER) | ||
| + "#if defined(_MSC_VER) && !defined(__clang__) | ||
| #include <intrin.h> | ||
| #else | ||
| #include <arm_acle.h> | ||
| @@ -285,7 +285,7 @@ macro(check_neon_ld4_intrinsics) | ||
| # Check whether compiler supports loading 4 neon vecs into a register range | ||
| set(CMAKE_REQUIRED_FLAGS "${NEONFLAG} ${NATIVEFLAG} ${ZNOLTOFLAG}") | ||
| check_c_source_compiles( | ||
| - "#if defined(_MSC_VER) && (defined(_M_ARM64) || defined(_M_ARM64EC)) | ||
| + "#if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_ARM64) || defined(_M_ARM64EC)) | ||
| # include <arm64_neon.h> | ||
| #else | ||
| # include <arm_neon.h> | ||
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate the decoded payload before timing it.
await res.text()only proves the body was consumable. If fetch stops honoringContent-Encoding: gzipand returns compressed bytes, this benchmark still records a number for the wrong path. Add a one-time preflight that/gzand/plainboth decode to the original HTML beforegroup()starts.Suggested guard
const base = `http://localhost:${server.port}`; + +const expectedText = html.toString(); +if ((await fetch(`${base}/gz`).then(res => res.text())) !== expectedText) { + throw new Error("gzip benchmark setup is not decoding the response body"); +} +if ((await fetch(`${base}/plain`).then(res => res.text())) !== expectedText) { + throw new Error("plain benchmark setup returned an unexpected body"); +} group("fetch + gzip decode", () => {🤖 Prompt for AI Agents