Skip to content
Closed
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
4 changes: 4 additions & 0 deletions packages/h3blast/src/h3blast.c
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ static void die(const char *fmt, ...) {
exit(1);
}

// The lshpack objects report a failed allocation of their Huffman tables through this function
// (LS_HPACK_OOM in scripts/build/deps/lshpack.ts). Bun's runtime defines it, and this program links without that.
void Bun__outOfMemory(void) { die("out of memory"); }

static uint64_t now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
--- a/lshpack.c 2026-05-02 06:41:46.290110113 +0000
+++ b/lshpack.c 2026-05-02 06:42:07.599902350 +0000
@@ -53,6 +53,103 @@
--- a/lshpack.c 2026-10-28 13:31:41.835028034 +0000
+++ b/lshpack.c 2026-10-28 15:53:38.267339303 +0000
@@ -52,6 +52,125 @@

#include "huff-tables.h"

+#if LS_HPACK_USE_LARGE_TABLES && LS_HPACK_BSS_LARGE_TABLES
+#if LS_HPACK_USE_LARGE_TABLES && LS_HPACK_LAZY_LARGE_TABLES
+/* Bun: hencs[65536] (512 KB) and hdecs[65536] (256 KB) are pure functions of
+ * encode_table[257]. Declare them in .bss instead of .rodata and fill once on
+ * first lshpack/lsqpack init — ~250 us, identical hot-path lookup afterwards.
+ * Non-static so lsqpack.c can extern the same arrays instead of carrying its
+ * encode_table[257]. They are left out of .rodata, and built once, at the
+ * first lshpack/lsqpack init, in memory that is allocated then: ~250 us,
+ * identical hot-path lookup afterwards. As zero-filled data of the executable
+ * (.bss) they made it large enough for a transparent huge page.
+ * Non-static so lsqpack.c can use the same tables instead of carrying its
+ * own copy. */
+struct henc { unsigned lens; uint32_t code; };
+struct hdec { uint8_t lens; uint8_t out[3]; };
+struct henc hencs[65536];
+struct hdec hdecs[65536];
+static int hpack_huff_tables_ready;
+struct lshpack_huff_tables { struct henc encs[65536]; struct hdec decs[65536]; };
+struct lshpack_huff_tables *lshpack_huff_tables;
+#define hencs (__atomic_load_n(&lshpack_huff_tables, __ATOMIC_RELAXED)->encs)
+#define hdecs (__atomic_load_n(&lshpack_huff_tables, __ATOMIC_RELAXED)->decs)
+#ifdef LS_HPACK_OOM
+/* Reports out of memory and does not return. */
+void LS_HPACK_OOM(void);
+#endif
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define HENC_I(i,j) (((j)<<8)|(i))
Expand All @@ -25,13 +32,23 @@
+void
+lshpack_huff_tables_init (void)
+{
+ /* Init is idempotent (every thread writes the same values), so a race only
+ * does redundant work — but the release/acquire pair ensures the table
+ * stores are visible before any later encode/decode load on another
+ * thread sees ready==1. */
+ if (__atomic_load_n(&hpack_huff_tables_ready, __ATOMIC_ACQUIRE))
+ struct lshpack_huff_tables *tables, *none = NULL;
+ struct { uint8_t bits, sym; } *lut;
+
+ if (__atomic_load_n(&lshpack_huff_tables, __ATOMIC_ACQUIRE))
+ return;
+
+ /* Both start zeroed: an entry of decs that stays zero takes the slow path. */
+ tables = calloc(1, sizeof(*tables));
+ lut = calloc(1 << 15, sizeof(*lut));
+ if (!tables || !lut)
+ {
+#ifdef LS_HPACK_OOM
+ LS_HPACK_OOM();
+#endif
+ abort();
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
+ }
+
+ /* hencs: combine two encode_table entries. {64,0} marks pairs whose
+ * combined width overflows 32 bits (handled by the slow loop). */
+ for (unsigned i = 0; i < 256; ++i)
Expand All @@ -41,7 +58,7 @@
+ for (unsigned j = 0; j < 256; ++j)
+ {
+ const unsigned bj = encode_table[j].bits;
+ struct henc *h = &hencs[HENC_I(i, j)];
+ struct henc *h = &tables->encs[HENC_I(i, j)];
+ if (bi + bj <= 32)
+ {
+ h->lens = bi + bj;
Expand All @@ -58,7 +75,6 @@
+ /* hdecs: for each 16-bit prefix, decode up to 3 symbols using a 15-bit
+ * direct LUT built from encode_table (codes <= 15 bits cover the common
+ * ASCII set; longer codes fall through to the slow path with lens==0). */
+ static struct { uint8_t bits, sym; } lut[1 << 15];
+ for (unsigned sym = 0; sym < 256; ++sym)
+ {
+ const unsigned b = encode_table[sym].bits;
Expand Down Expand Up @@ -89,49 +105,55 @@
+ }
+ if (n)
+ {
+ hdecs[idx].lens = (uint8_t)(((16 - bits_left) << 2) | n);
+ hdecs[idx].out[0] = out[0];
+ hdecs[idx].out[1] = out[1];
+ hdecs[idx].out[2] = out[2];
+ tables->decs[idx].lens = (uint8_t)(((16 - bits_left) << 2) | n);
+ tables->decs[idx].out[0] = out[0];
+ tables->decs[idx].out[1] = out[1];
+ tables->decs[idx].out[2] = out[2];
+ }
+ /* else: leave zeroed (.bss) — lens==0 triggers slow path */
+ /* else: leave zeroed — lens==0 triggers slow path */
+ }
+ free(lut);
+
+ __atomic_store_n(&hpack_huff_tables_ready, 1, __ATOMIC_RELEASE);
+ /* Two threads can get here at once, and each has built the same tables.
+ * The first to publish keeps its own; a later encode/decode load on any
+ * thread comes after that thread's own call returned, so it sees them. */
+ if (!__atomic_compare_exchange_n(&lshpack_huff_tables, &none, tables, 0,
+ __ATOMIC_RELEASE, __ATOMIC_ACQUIRE))
+ free(tables);
+}
+#endif
+
#define HPACK_STATIC_TABLE_SIZE 61
#define INITIAL_DYNAMIC_TABLE_SIZE 4096

@@ -291,6 +384,9 @@
@@ -290,6 +409,9 @@
int
lshpack_enc_init (struct lshpack_enc *enc)
{
+#if LS_HPACK_USE_LARGE_TABLES && LS_HPACK_BSS_LARGE_TABLES
+#if LS_HPACK_USE_LARGE_TABLES && LS_HPACK_LAZY_LARGE_TABLES
+ lshpack_huff_tables_init();
+#endif
struct lshpack_double_enc_head *buckets;
unsigned nbits = 2;
unsigned i;
@@ -1270,6 +1366,9 @@
@@ -1269,6 +1391,9 @@
void
lshpack_dec_init (struct lshpack_dec *dec)
{
+#if LS_HPACK_USE_LARGE_TABLES && LS_HPACK_BSS_LARGE_TABLES
+#if LS_HPACK_USE_LARGE_TABLES && LS_HPACK_LAZY_LARGE_TABLES
+ lshpack_huff_tables_init();
+#endif
memset(dec, 0, sizeof(*dec));
dec->hpd_max_capacity = INITIAL_DYNAMIC_TABLE_SIZE;
dec->hpd_cur_max_capacity = INITIAL_DYNAMIC_TABLE_SIZE;
--- a/huff-tables.h 2026-05-02 06:41:46.300110018 +0000
+++ b/huff-tables.h 2026-05-02 06:42:07.509903227 +0000
--- a/huff-tables.h 2026-10-28 13:31:41.843028034 +0000
+++ b/huff-tables.h 2026-10-28 13:32:33.559029924 +0000
@@ -266,7 +266,7 @@
};


-#if LS_HPACK_USE_LARGE_TABLES
+#if LS_HPACK_USE_LARGE_TABLES && !LS_HPACK_BSS_LARGE_TABLES
+#if LS_HPACK_USE_LARGE_TABLES && !LS_HPACK_LAZY_LARGE_TABLES
#if __BYTE_ORDER == __LITTLE_ENDIAN
#define I(i,j) ((j<<8)|i)
#else
Expand All @@ -140,7 +162,7 @@


-#if LS_HPACK_USE_LARGE_TABLES
+#if LS_HPACK_USE_LARGE_TABLES && !LS_HPACK_BSS_LARGE_TABLES
+#if LS_HPACK_USE_LARGE_TABLES && !LS_HPACK_LAZY_LARGE_TABLES
static const struct hdec { uint8_t lens; uint8_t out[3]; } hdecs[] =
{
/*
49 changes: 0 additions & 49 deletions patches/lsqpack/bss-huff-tables.patch

This file was deleted.

51 changes: 51 additions & 0 deletions patches/lsqpack/lazy-huff-tables.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
--- a/lsqpack.c 2026-10-28 13:33:26.015031842 +0000
+++ b/lsqpack.c 2026-10-28 15:53:38.559339313 +0000
@@ -69,6 +69,16 @@
#include <xxhash.h>
#endif

+#if LS_QPACK_USE_LARGE_TABLES && LS_HPACK_LAZY_LARGE_TABLES
+/* Bun: share the hencs/hdecs that lshpack builds at its first init (same
+ * RFC 7541 Huffman code) instead of carrying a second 768 KB copy in .rodata. */
+struct henc { unsigned lens; uint32_t code; };
+struct hdec { uint8_t lens; uint8_t out[3]; };
+extern struct lshpack_huff_tables { struct henc encs[65536]; struct hdec decs[65536]; } *lshpack_huff_tables;
+#define hencs (__atomic_load_n(&lshpack_huff_tables, __ATOMIC_RELAXED)->encs)
+#define hdecs (__atomic_load_n(&lshpack_huff_tables, __ATOMIC_RELAXED)->decs)
+extern void lshpack_huff_tables_init(void);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
+#endif
#include "huff-tables.h"

#define MIN(a, b) ((a) < (b) ? (a) : (b))
@@ -486,6 +496,10 @@
void
lsqpack_enc_preinit (struct lsqpack_enc *enc, void *logger_ctx)
{
+#if LS_QPACK_USE_LARGE_TABLES && LS_HPACK_LAZY_LARGE_TABLES
+ /* An encoder that is only preinitialized encodes with the tables too. */
+ lshpack_huff_tables_init();
+#endif
memset(enc, 0, sizeof(*enc));
STAILQ_INIT(&enc->qpe_all_entries);
STAILQ_INIT(&enc->qpe_hinfo_arrs);
@@ -2775,6 +2789,9 @@
unsigned dyn_table_size, unsigned max_risked_streams,
const struct lsqpack_dec_hset_if *dh_if, enum lsqpack_dec_opts opts)
{
+#if LS_QPACK_USE_LARGE_TABLES && LS_HPACK_LAZY_LARGE_TABLES
+ lshpack_huff_tables_init();
+#endif
unsigned i;
memset(dec, 0, sizeof(*dec));
dec->qpd_opts = opts;
--- a/huff-tables.h 2026-10-28 13:33:26.023031842 +0000
+++ b/huff-tables.h 2026-10-28 13:32:45.863030374 +0000
@@ -5141,7 +5141,7 @@
},
};

-#if LS_QPACK_USE_LARGE_TABLES
+#if LS_QPACK_USE_LARGE_TABLES && !LS_HPACK_LAZY_LARGE_TABLES
#if __BYTE_ORDER == __LITTLE_ENDIAN
#define I(i,j) ((j<<8)|i)
#else
16 changes: 10 additions & 6 deletions scripts/build/deps/lshpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ export const lshpack: Dependency = {
}),

// hencs[65536]/hdecs[65536] (768 KB of .rodata) are pure functions of the
// 257-entry encode_table. Declare them in .bss and fill on first init
// (~250 us once); the hot-path lookup is unchanged.
patches: ["patches/lshpack/bss-huff-tables.patch"],
// 257-entry encode_table. Build them at the first init (~250 us once) in
// memory allocated then; the hot-path lookup is unchanged. They are not
// zero-filled data of the executable (.bss): that has to stay smaller than a
// transparent huge page (test/internal/zero-filled-data-below-a-huge-page.test.ts).
patches: ["patches/lshpack/lazy-huff-tables.patch"],

build: cfg => {
// <sys/queue.h> ships with glibc and BSD libc but not musl or win32.
Expand All @@ -55,13 +57,15 @@ export const lshpack: Dependency = {
...lsxpackDefines,
XXH_HEADER_NAME: "xxhash.h",
// lshpack.c defaults LS_HPACK_USE_LARGE_TABLES=1 internally; setting
// it here is purely defensive so the bss-huff-tables patch can't be
// it here is purely defensive so the lazy-huff-tables patch can't be
// silently disabled by a future upstream change to the default.
LS_HPACK_USE_LARGE_TABLES: 1,
LS_HPACK_BSS_LARGE_TABLES: 1,
LS_HPACK_LAZY_LARGE_TABLES: 1,
},
};
if (cfg.windows) spec.cflags = ["-w"];
// The patch reports a failed allocation of the tables through this function.
// The value is a function name, and a `defines` entry would quote it.
spec.cflags = ["-DLS_HPACK_OOM=Bun__outOfMemory", ...(cfg.windows ? ["-w"] : [])];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
return spec;
},

Expand Down
6 changes: 3 additions & 3 deletions scripts/build/deps/lsqpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export const lsqpack: Dependency = {
commit: LSQPACK_COMMIT,
}),

// QPACK uses the same RFC 7541 Huffman code as HPACK; share lshpack's
// .bss-backed hencs/hdecs instead of carrying a second 768 KB copy.
// QPACK uses the same RFC 7541 Huffman code as HPACK; share the hencs/hdecs
// that lshpack builds at its first init instead of carrying a second 768 KB copy.
patches: [
"patches/lsqpack/bss-huff-tables.patch",
"patches/lsqpack/lazy-huff-tables.patch",
// The decoder asks dhi_prepare_decode() for room for the bytes it has in
// hand, so the buffer of a long name or value grows as it arrives.
"patches/lsqpack/decode-room-for-bytes-in-hand.patch",
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/deps/lsquic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export const lsquic: Dependency = {
// lsqpack.c defaults LS_QPACK_USE_LARGE_TABLES=1 internally; setting
// it here is defensive (mirrors lshpack.ts).
LS_QPACK_USE_LARGE_TABLES: 1,
LS_HPACK_BSS_LARGE_TABLES: 1,
LS_HPACK_LAZY_LARGE_TABLES: 1,
LSQPACK_ENC_LOGGER_HEADER: "lsquic_qpack_enc_logger.h",
LSQPACK_DEC_LOGGER_HEADER: "lsquic_qpack_dec_logger.h",
LSQUIC_DEBUG_NEXT_ADV_TICK: 0,
Expand Down
36 changes: 36 additions & 0 deletions test/internal/zero-filled-data-below-a-huge-page.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Where /sys/kernel/mm/transparent_hugepage/enabled is `always`, the kernel backs a stretch of anonymous memory that has
// the size and alignment of a huge page (2 MB) with one huge page, at the first write to it. Bun opts its heaps out
// (MADV_NOHUGEPAGE). It can't do that in time for the executable's zero-filled data (.bss): the kernel maps it, and the
// dynamic loader writes to it (it copies environ, stdin, stdout and stderr there) before any code of Bun runs.
//
// A mapping smaller than a huge page holds no such stretch, wherever the link puts it. At 2 MB or more it depends on the
// address the link gave .bss whether every process takes 2 MB more, so any change to Bun can switch that on, and
// test/aot/perf/start.test.ts fails for that build alone.
//
// When this fails, allocate a large table at its first use instead of declaring it zero-filled.
import { expect, test } from "bun:test";
import { bunExe, isASAN, isDebug, isLinux, readElf64ProgramHeaders } from "harness";

const smallestPage = 4096n;
const hugePage = 2 * 1024 * 1024;

// The anonymous mappings that the kernel makes for an ELF executable: what each writable PT_LOAD segment has in memory
// past the last page of its data in the file.
function sizesOfZeroFilledMappings(path: string) {
const [PT_LOAD, PF_W] = [1, 2];
const pageUp = (address: bigint) => ((address + smallestPage - 1n) / smallestPage) * smallestPage;
return readElf64ProgramHeaders(path)
.filter(segment => segment.type === PT_LOAD && (segment.flags & PF_W) !== 0)
.map(segment => Number(pageUp(segment.vaddr + segment.memsz) - pageUp(segment.vaddr + segment.filesz)))
.filter(size => size > 0);
}

// A debug build and AddressSanitizer make .bss several times larger, and nobody measures their memory.
test.skipIf(!isLinux || isDebug || isASAN)(
"the executable's zero-filled data is smaller than a transparent huge page",
() => {
const sizes = sizesOfZeroFilledMappings(bunExe());
expect(sizes.length).toBeGreaterThan(0);
expect(Math.max(...sizes)).toBeLessThan(hugePage);
},
);
Loading