Rewrite Bun.escapeHTML using Highway SIMD (#31483)
Rewrites `Bun.escapeHTML` from Rust into a dedicated C++ binding (`src/jsc/bindings/escapeHTML.cpp`) using Highway SIMD, and makes it faster than both the previous Rust and Zig implementations in every measured case. ## What changed **New C++ binding** — `jsFunctionBunEscapeHTML`, structured like `stringWidth.cpp`: - Coerces the argument to a string, branches on 8-bit (Latin-1) vs 16-bit (UTF-16) backing. - **Passthrough (nothing to escape): returns the input `JSString` unchanged** — zero allocation. A SWAR (8-bytes-at-a-time) scan handles short strings without SIMD-dispatch overhead; long strings use a Highway kernel. - **Escape path**: one SIMD pass computes the exact escaped length, the result is allocated once via `WTF::String::tryCreateUninitialized`, and a single **table-driven** scalar pass fills it (a 256-entry length table → one load + branch per character; entities written directly). An explicit `outLength > String::MaxLength` check throws `OutOfMemory` for pathologically large output. **Highway SIMD kernels** in `highway_strings.cpp` (dispatched via `HWY_DYNAMIC_DISPATCH`): - `IndexOfHTMLEscapeChar8Impl` / `IndexOfHTMLEscapeChar16Impl` — first-metacharacter scan. - `HtmlEscapeExtraLen8Impl` / `HtmlEscapeExtraLen16Impl` — exact escaped-length count. ## Behavior Unchanged from the previous implementation: - The five entities: `&`→`&`, `<`→`<`, `>`→`>`, `"`→`"`, `'`→`'` (numeric, not `'`). - UTF-16 surrogate pairs and lone surrogates are copied through verbatim (they're > 0x80 and can't match a metacharacter) — only markup characters change. - Non-string arguments are coerced (`number`/`boolean`/`null`/`undefined` short-circuit; a `Symbol` still throws `TypeError`). - `Bun.escapeHTML` remains a `function` with `.length === 2`. ## Deletions - The Rust implementation (`escape_html_for_latin1_input`, `escape_html_for_utf16_input`, `Escaped<T>`, scalar helpers), the `Bun__escapeHTML8`/`Bun__escapeHTML16` FFI, the orphaned `ScalarVec`/`AsciiVector` SIMD stand-ins, and the dead `functionBunEscapeHTMLWithoutTypeCheck` declaration. **Kept:** the byte→entity lookups `html_escape_entity` / `xml_escape_entity` — the markdown renderer, SSR attribute escaping, and the `bun test` JUnit XML reporter still use them. ## Benchmarks Linux x64 (Intel Xeon Platinum 8375C), release builds, `bench/snippets/escapeHTML.mjs`, `mitata`. main = `db928c247` (Rust), this PR C++, and Bun `v1.3.14` (the last Zig release). Lower is better; the last two columns are this PR's speedup. | input | v1.3.14 (Zig) | main (Rust) | this PR (C++) | PR vs main | PR vs 1.3.14 | |---|--:|--:|--:|--:|--:| | latin1 short, escapes (29 chars) | 109 ns | 96 ns | 77 ns | **1.24× faster** | **1.41× faster** | | latin1 short, passthrough (26 chars) | 24 ns | 22 ns | 15 ns | **1.44× faster** | **1.53× faster** | | latin1 long, escapes (~57 KB) | 109 µs | 81 µs | 42 µs | **1.94× faster** | **2.63× faster** | | latin1 long, passthrough (~56 KB) | 4.1 µs | 29 µs | 1.2 µs | **25× faster** | **3.6× faster** | | utf16 short, escapes (25 chars) | 201 ns | 165 ns | 87 ns | **1.90× faster** | **2.31× faster** | | utf16 short, passthrough (24 chars) | 40 ns | 37 ns | 21 ns | **1.75× faster** | **1.88× faster** | | utf16 long, escapes (~60 KB) | 170 µs | 120 µs | 52 µs | **2.31× faster** | **3.27× faster** | | utf16 long, passthrough (~58 KB) | 25 µs | 55 µs | 2.4 µs | **23× faster** | **10.7× faster** | ## Verification - `test/js/bun/util/escapeHTML.test.js` — all pass under `bun bd` (ASAN), including the existing latin1/utf16 fuzz and lone-surrogate cases. Added coverage for: every metacharacter, argument coercion (incl. the symbol-throws path), zero-copy passthrough, escapes at SIMD lane boundaries (offsets around 8/16/32/64), and densely packed escapes. - Consumers of the kept helpers pass: markdown (`test/cli/run/markdown-entrypoint.test.ts`) and the JUnit reporter (`test/js/junit-reporter/junit.test.js`). - New SIMD kernels added to the three `scripts/verify-baseline-static/allowlist-*.txt` baseline static-scan allowlists. ## Related issues Fixes #21818 — the reported segfault was in `escapeHTMLForLatin1Input`; that code path is replaced here by a bounds-checked C++ implementation that passes the existing "bad input doesn't crash" fuzz test (lone surrogates and `0xff` bytes at every offset) clean under ASAN. Contributes to #8782 (switch from Zig `@Vector` to Google Highway with runtime dispatch) — this migrates `Bun.escapeHTML`. Other `@Vector` call sites remain, so that tracking issue stays open. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
R
robobun committed
472a06a2fb33587126ff6115dbecc9fbb4416f79
Parent: 4ee835b
Committed by GitHub <noreply@github.com>
on 5/28/2026, 4:14:58 AM