Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ Third-party C/C++ libraries are vendored locally and can be read from disk (thes
- `vendor/tinycc/` - TinyCC (FFI JIT compiler, fork: oven-sh/tinycc)
- `vendor/WebKit/` - WebKit/JavaScriptCore (JS engine)
- `vendor/zig/` - Zig compiler/stdlib
- `vendor/zlib/` - zlib (compression, cloudflare fork)
- `vendor/zlib/` - zlib-ng (compression, zlib-compat mode)
- `vendor/zstd/` - Zstandard (compression)

Build configuration for these is in `scripts/build/deps/*.ts`.
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Bun statically links these libraries:
| [`simdutf`](https://github.com/simdutf/simdutf) | Apache 2.0 |
| [`tinycc`](https://github.com/tinycc/tinycc) | LGPL v2.1 |
| [`uSockets`](https://github.com/uNetworking/uSockets) | Apache 2.0 |
| [`zlib-cloudflare`](https://github.com/cloudflare/zlib) | zlib |
| [`zlib-ng`](https://github.com/zlib-ng/zlib-ng) | zlib |
| [`c-ares`](https://github.com/c-ares/c-ares) | MIT licensed |
| [`libicu`](https://github.com/unicode-org/icu) 72 | [license here](https://github.com/unicode-org/icu/blob/main/icu4c/LICENSE) |
| [`libbase64`](https://github.com/aklomp/base64/blob/master/LICENSE) | BSD 2-Clause |
Expand Down
36 changes: 36 additions & 0 deletions bench/snippets/fetch-gzip.mjs
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();
});
Comment on lines +25 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the decoded payload before timing it.

await res.text() only proves the body was consumable. If fetch stops honoring Content-Encoding: gzip and returns compressed bytes, this benchmark still records a number for the wrong path. Add a one-time preflight that /gz and /plain both decode to the original HTML before group() 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
Verify each finding against the current code and only fix it if needed.

In `@bench/snippets/fetch-gzip.mjs` around lines 25 - 33, Add a one-time preflight
before calling group("fetch + gzip decode", ...) that fetches `${base}/gz` and
`${base}/plain`, awaits res.text() for each, and validates the decoded strings
match the expected HTML payload (or that both decode to the same canonical plain
response); if they do not match, throw or fail the run so the benches inside
bench(...) do not record misleading numbers. Use the existing symbols (group,
bench, fetch, res.text, base, paths /gz and /plain) to locate where to insert
the preflight and perform the validation.

});

await run();
167 changes: 167 additions & 0 deletions bench/snippets/zlib-comprehensive.mjs
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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 drain() ignores total bytes entirely. Warm each corpus once and assert the decompressed length or hash matches the source before running the timed groups.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bench/snippets/zlib-comprehensive.mjs` around lines 54 - 165, The benchmark
currently discards outputs (especially in streaming where drain() ignores
bytes), so add a correctness warm-up that for each corpus and for gzipped
entries performs a one-time roundtrip check before running timed groups: for
sync cases call zlib.gzipSync/zlib.gunzipSync and assert output length or hash
equals the original; for async helpers (gzip, gunzip) await a single call and
assert equality; for streams createGzip/createGunzip run pipeline once with
Readable.from(chunks) and a drain that verifies total bytes or computes a
checksum, throwing or console.error on mismatch to fail fast (refer to drain(),
gzip, gunzip, zlib.gzipSync, zlib.gunzipSync, pipeline,
streamInputs/streamGzInputs and the bench group names).


await run();
2 changes: 1 addition & 1 deletion docs/project/license.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Bun statically links these libraries:
| [`simdutf`](https://github.com/simdutf/simdutf) | Apache 2.0 |
| [`tinycc`](https://github.com/tinycc/tinycc) | LGPL v2.1 |
| [`uSockets`](https://github.com/uNetworking/uSockets) | Apache 2.0 |
| [`zlib-cloudflare`](https://github.com/cloudflare/zlib) | zlib |
| [`zlib-ng`](https://github.com/zlib-ng/zlib-ng) | zlib |
| [`c-ares`](https://github.com/c-ares/c-ares) | MIT licensed |
| [`libicu`](https://github.com/unicode-org/icu) 72 | [license here](https://github.com/unicode-org/icu/blob/main/icu4c/LICENSE) |
| [`libbase64`](https://github.com/aklomp/base64/blob/master/LICENSE) | BSD 2-Clause |
Expand Down
8 changes: 0 additions & 8 deletions patches/zlib/CMakeLists.txt.patch

This file was deleted.

55 changes: 55 additions & 0 deletions patches/zlib/clang-cl-arm64.patch
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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Verify each finding against the current code and only fix it if needed.

In `@patches/zlib/clang-cl-arm64.patch` at line 13, Update the patch metadata to
record the upstream tracking reference: open an issue or PR in the zlib-ng
upstream repo describing the clang-cl ARM64 failure and this local fix, then
edit the patch header in clang-cl-arm64.patch (the line that currently reads
"Upstream: not yet reported (zlib-ng CI does not cover clang-cl ARM64).") to
include the upstream issue/PR URL and ID plus a brief one-line summary of the
upstream ticket; keep the existing note and [request_verification] tag so future
bumps can determine whether the fix was merged, reverted, or superseded.

--- 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>
26 changes: 0 additions & 26 deletions patches/zlib/deflate.h.patch

This file was deleted.

11 changes: 0 additions & 11 deletions patches/zlib/ucm.cmake.patch

This file was deleted.

10 changes: 7 additions & 3 deletions scripts/build/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,13 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput {
n.comment("─── Dependencies ───");
n.blank();
const deps: ResolvedDep[] = [];
const depsByName = new Map<string, ResolvedDep>();
for (const dep of allDeps) {
const resolved = resolveDep(n, cfg, dep);
if (resolved !== null) deps.push(resolved);
const resolved = resolveDep(n, cfg, dep, depsByName);
if (resolved !== null) {
deps.push(resolved);
depsByName.set(dep.name, resolved);
}
}

// Collect all dep lib paths, include dirs, output stamps, and directly-
Expand Down Expand Up @@ -449,7 +453,7 @@ function emitZigOnly(n: Ninja, cfg: Config, sources: Sources): BunOutput {

// Only dep: zstd, for @cImport headers. resolveDep emits its
// fetch/configure/build; emitZig only depends on the fetch stamp.
const zstdDep = resolveDep(n, cfg, zstd);
const zstdDep = resolveDep(n, cfg, zstd, new Map());
assert(zstdDep !== null, "zstd resolveDep returned null — should never be skipped");

// Codegen: emitted fully, but only zigInputs/zigOrderOnly are pulled.
Expand Down
16 changes: 9 additions & 7 deletions scripts/build/deps/libarchive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import type { Dependency } from "../source.ts";
import { depSourceDir } from "../source.ts";
import { depBuildDir } from "../source.ts";

const LIBARCHIVE_COMMIT = "ded82291ab41d5e355831b96b0e1ff49e24d8939";

Expand All @@ -35,9 +35,11 @@ export const libarchive: Dependency = {
],

// libarchive's configure-time check_include_file("zlib.h") needs zlib's
// headers on disk. We don't LINK zlib into libarchive (ENABLE_ZLIB=OFF) —
// we just need the compile-time knowledge that deflate exists so
// libarchive compiles its gzip filter instead of fork/exec'ing gzip(1).
// headers on disk. zlib-ng generates zlib.h during its own cmake configure,
// so we need zlib BUILT (not just fetched) before libarchive configures.
// We don't LINK zlib into libarchive (ENABLE_ZLIB=OFF) — we just need the
// compile-time knowledge that deflate exists so libarchive compiles its
// gzip filter instead of fork/exec'ing gzip(1).
fetchDeps: ["zlib"],

build: cfg => ({
Expand All @@ -46,9 +48,9 @@ export const libarchive: Dependency = {
pic: true,
libSubdir: "libarchive",

// -I into zlib's SOURCE dir (vendor/zlib/). This is why fetchDeps exists:
// the zlib source must be on disk before libarchive's configure runs.
extraCFlags: [`-I${depSourceDir(cfg, "zlib")}`],
// -I into zlib's BUILD dir (zlib-ng generates zlib.h there at configure
// time). zlib must be built before libarchive's configure runs.
extraCFlags: [`-I${depBuildDir(cfg, "zlib")}`],

args: {
ENABLE_INSTALL: "OFF",
Expand Down
Loading
Loading