Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5c4abf6
xml: SIMD structural index (stage 1) + tape-row output for the XML pa…
Jarred-Sumner Aug 7, 2026
bbfdc48
xml: iterative element loop, streamed attributes, fast tag/content/at…
Jarred-Sumner Aug 7, 2026
42f4898
rows→JS in one C++ call using the VM's JSON atom-string cache; skip U…
Jarred-Sumner Aug 7, 2026
a077a54
Recycle a per-thread arena for Bun.{XML,JSONC,TOML,YAML,JSON5}.parse;…
Jarred-Sumner Aug 7, 2026
cf1c4f4
bench/xml: lockfile
Jarred-Sumner Aug 7, 2026
228880f
xml: review fixes — non-characters after '>' in attribute values, bou…
Jarred-Sumner Aug 7, 2026
072d131
bench: drop the accidentally added copy of the old parser
Jarred-Sumner Aug 7, 2026
4260651
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
9493755
clippy: unwrap_or_default
Jarred-Sumner Aug 7, 2026
704d974
verify-baseline: allowlist the XML index kernel's Highway dispatch ta…
Jarred-Sumner Aug 7, 2026
19dc05c
rows→JS: three string paths (UTF-8 / Latin-1 / UTF-16 spans) keyed on…
Jarred-Sumner Aug 7, 2026
224949f
bench: drop the re-added copy of the old parser
Jarred-Sumner Aug 7, 2026
20dc228
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
2b2858a
docs(xml): performance section with the JS and native benchmark tables
Jarred-Sumner Aug 7, 2026
bbb13a8
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
2eff1ba
xml: parse 16-bit JS strings natively — the scanner, sinks and struct…
Jarred-Sumner Aug 7, 2026
38b13f4
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
8a6e7b1
xml: keep the memcmp-free name comparison generic over the code unit
Jarred-Sumner Aug 7, 2026
d7b33b8
docs(xml): refresh the JS benchmark table after the in-place string p…
Jarred-Sumner Aug 7, 2026
f40f8db
review nits: stale doc comments, empty-array expansion under bash 3.2…
Jarred-Sumner Aug 7, 2026
8e0b47e
xml: SIMD stage 1 for UTF-16 code units too (XmlIndex16Impl: low byte…
Jarred-Sumner Aug 7, 2026
30762af
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
b2382c7
verify-baseline: feature ceilings for the refactored XML kernels (VL/…
Jarred-Sumner Aug 7, 2026
3767a01
verify-baseline: rebuild the aarch64 allowlist from main plus the XML…
Jarred-Sumner Aug 7, 2026
5c36d2f
xml: reject lone surrogates in 16-bit strings (flagged by both UTF-16…
Jarred-Sumner Aug 7, 2026
d50527c
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
ee1d723
Release the input string's ref in the text-format scaffold (SliceWith…
Jarred-Sumner Aug 8, 2026
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
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions bench/xml/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bench/xml/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"name": "xml-benchmark",
"version": "1.0.0",
"dependencies": {
"@xmldom/xmldom": "^0.9.10",
"fast-xml-parser": "^5.2.5",
"txml": "^6.0.0",
"xml2js": "^0.6.2"
}
}
22 changes: 19 additions & 3 deletions bench/xml/xml.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { DOMParser } from "@xmldom/xmldom";
import { XMLBuilder, XMLParser } from "fast-xml-parser";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import * as txml from "txml";
import xml2js from "xml2js";
import { bench, group, run } from "../runner.mjs";

Expand Down Expand Up @@ -69,18 +73,30 @@ const parseXml2js = doc => {
return result;
};

for (const [label, doc] of [
const docs = [
["small", small],
["S3 listing", large],
["Atom feed", mixed],
]) {
];
// Real-world files: every *.xml / *.svg in $BUN_XML_BENCH_FIXTURES.
if (process.env.BUN_XML_BENCH_FIXTURES) {
const dir = process.env.BUN_XML_BENCH_FIXTURES;
for (const f of readdirSync(dir).sort()) {
if (/\.(xml|svg)$/.test(f)) docs.push([f, readFileSync(join(dir, f), "utf8")]);
Comment on lines +82 to +85

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Define one strict BUN_XML_BENCH_FIXTURES contract.

The JavaScript runner treats an empty value as unset and accepts relative paths. The Rust runner treats an empty value as an unreadable directory, ignores that error, and can panic while reading a selected fixture. The same configuration can therefore benchmark different corpora or silently fall back to synthetic inputs.

  • bench/xml/xml.mjs#L82-L85: Distinguish unset from empty, reject an empty configured value, and resolve the configured directory to an absolute path before file operations.
  • src/parsers/benches/xml_parse.rs#L23-L43: Apply the same empty and absolute-path policy. Propagate directory enumeration, entry, and file-read errors instead of using if let Ok, filter_map(|e| e.ok()), and unwrap().

As per coding guidelines, use absolute file paths, distinguish empty and unset input, and propagate I/O failures explicitly.

📍 Affects 2 files
  • bench/xml/xml.mjs#L82-L85 (this comment)
  • src/parsers/benches/xml_parse.rs#L23-L43
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bench/xml/xml.mjs` around lines 82 - 85, Define one strict
BUN_XML_BENCH_FIXTURES contract across both runners: in bench/xml/xml.mjs lines
82-85, distinguish unset from an empty value, reject empty configuration, and
resolve the configured directory to an absolute path before file operations; in
src/parsers/benches/xml_parse.rs lines 23-43, apply the same policy and
propagate directory enumeration, entry, and file-read errors instead of silently
ignoring failures or unwrapping results.

Source: Coding guidelines

}
}
const xmldom = new DOMParser();

for (const [label, doc] of docs) {
group(`parse ${label} (${sizeLabel(doc.length)})`, () => {
if (isBun) {
bench("Bun.XML.parse", () => Bun.XML.parse(doc));
bench("Bun.XML.parse { compact: false }", () => Bun.XML.parse(doc, { compact: false }));
}
bench("txml", () => txml.parse(doc));
bench("fast-xml-parser", () => fxp.parse(doc));
bench("xml2js", () => parseXml2js(doc));
bench("@xmldom/xmldom DOMParser", () => xmldom.parseFromString(doc, "text/xml"));
if (doc.length < 512 * 1024) bench("xml2js", () => parseXml2js(doc));
});
}

Expand Down
40 changes: 34 additions & 6 deletions scripts/bench-json-rust.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# Build + run the JSON parser criterion bench (src/parsers/benches/json_parse.rs): compiles the
# native pieces the parser reaches into one archive and points RUSTFLAGS at it. Needs `bun bd` once.
# Build + run the JSON parser criterion bench (src/parsers/benches/json_parse.rs), or with `--xml`
# the XML one (benches/xml_parse.rs): compiles the native pieces the parsers reach into one archive
# and points RUSTFLAGS at it. Needs `bun bd` once. `--test` runs the crate's unit tests instead.
set -euo pipefail
cd "$(dirname "$0")/.."

Expand Down Expand Up @@ -41,9 +42,30 @@ build "$SUP/simdutf_shim.o" $CXX -O3 -fPIC -std=c++20 -I"$SUP" -c src/parsers/be
for f in abort targets per_target print timer nanobenchmark aligned_allocator; do
build "$SUP/hwy_$f.o" $CXX -O3 -fPIC -std=c++17 -Ivendor/highway -c "vendor/highway/hwy/$f.cc"
done
if [ -f src/jsc/bindings/highway_json.cpp ]; then
$CXX -O3 -fPIC -std=c++17 -Ivendor/highway -Isrc/jsc/bindings -I"$BUN_CODEGEN_DIR" -c src/jsc/bindings/highway_json.cpp -o "$SUP/highway_json.o"
for k in json xml; do
if [ -f src/jsc/bindings/highway_$k.cpp ]; then
$CXX -O3 -fPIC -std=c++17 -Ivendor/highway -Isrc/jsc/bindings -I"$BUN_CODEGEN_DIR" -c src/jsc/bindings/highway_$k.cpp -o "$SUP/highway_$k.o"
fi
done
# C/C++ XML parsers for benches/xml_parse.rs to compare against (optional).
XML_C_DEFS=()
XML_C_LIBS=()
PUGI_VERSION=1.14
if [ ! -f "$SUP/pugixml-$PUGI_VERSION/src/pugixml.cpp" ]; then
curl -fsSL "https://github.com/zeux/pugixml/releases/download/v$PUGI_VERSION/pugixml-$PUGI_VERSION.tar.gz" | tar -xz -C "$SUP" || true

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report the skipped pugixml comparison.

|| true converts a failed download or extraction into a successful script result. The benchmark then runs without identifying that it omitted pugixml. Keep the optional fallback, but print a warning that names the failed dependency and gives a retry action.

As per coding guidelines, never swallow failures or signal success after failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench-json-rust.sh` at line 55, Update the pugixml download and
extraction command in the benchmark setup to retain the optional fallback while
explicitly warning when it fails; include “pugixml” in the warning and advise
retrying the benchmark or dependency download, without silently treating the
failure as success.

Source: Coding guidelines

fi
if [ -f "$SUP/pugixml-$PUGI_VERSION/src/pugixml.cpp" ]; then
build "$SUP/pugixml.o" $CXX -O3 -fPIC -std=c++17 -DNDEBUG -c "$SUP/pugixml-$PUGI_VERSION/src/pugixml.cpp"
XML_C_DEFS+=(-DHAVE_PUGIXML "-I$SUP/pugixml-$PUGI_VERSION/src")
fi
if [ -f /usr/include/expat.h ]; then XML_C_DEFS+=(-DHAVE_EXPAT); XML_C_LIBS+=(-Clink-arg=-lexpat); fi
if [ -d /usr/include/libxml2 ]; then XML_C_DEFS+=(-DHAVE_LIBXML2 -I/usr/include/libxml2); XML_C_LIBS+=(-Clink-arg=-lxml2); fi
$CXX -O3 -fPIC -std=c++17 "${XML_C_DEFS[@]}" -c src/parsers/benches/support/xml_c_shim.cpp -o "$SUP/xml_c_shim.o"
XML_CFG=()
for d in "${XML_C_DEFS[@]}"; do
case "$d" in -DHAVE_*) XML_CFG+=("--cfg" "$(echo "${d#-DHAVE_}" | tr A-Z a-z)") ;; esac
done

Comment thread
claude[bot] marked this conversation as resolved.
rm -f "$SUP/libbun_bench_cdeps.a"
ar rcs "$SUP/libbun_bench_cdeps.a" "$SUP"/*.o
ranlib "$SUP/libbun_bench_cdeps.a"
Expand All @@ -52,10 +74,16 @@ export MIMALLOC_PURGE_DELAY=${MIMALLOC_PURGE_DELAY:-2000}
export BUN_JSON_BENCH_FIXTURES=${BUN_JSON_BENCH_FIXTURES:-$PWD/bench/json-corpus}
CXXLIB=stdc++
[ "$(uname -s)" = Darwin ] && CXXLIB=c++
export RUSTFLAGS="${RUSTFLAGS:-} -Clink-arg=$PWD/$SUP/libbun_bench_cdeps.a -Clink-arg=-l$CXXLIB -Clink-arg=-lm -Clink-arg=-ldl -Clink-arg=-lpthread -Clink-arg=-lc"
export BUN_XML_BENCH_FIXTURES=${BUN_XML_BENCH_FIXTURES:-$PWD/bench/xml-corpus}
export RUSTFLAGS="${RUSTFLAGS:-} ${XML_CFG[*]} -Clink-arg=$PWD/$SUP/libbun_bench_cdeps.a ${XML_C_LIBS[*]} -Clink-arg=-l$CXXLIB -Clink-arg=-lm -Clink-arg=-ldl -Clink-arg=-lpthread -Clink-arg=-lc"

if [ "${1:-}" = "--test" ]; then
shift
exec cargo test -p bun_parsers --lib --release "$@"
fi
exec cargo bench -p bun_parsers --bench json_parse "$@"
BENCH=json_parse
if [ "${1:-}" = "--xml" ]; then
shift
BENCH=xml_parse
fi
exec cargo bench -p bun_parsers --bench "$BENCH" "$@"
1 change: 1 addition & 0 deletions scripts/build/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput {
// file"). It only includes highway + libc headers anyway.
if (cfg.debug) {
noPchSources.add(resolve(cfg.cwd, "src/jsc/bindings/highway_json.cpp"));
noPchSources.add(resolve(cfg.cwd, "src/jsc/bindings/highway_xml.cpp"));
}

// Windows-only cpp sources (rescle — PE resource editor for --compile).
Expand Down
5 changes: 5 additions & 0 deletions scripts/build/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { writeIfChanged } from "./fs.ts";
import { generateJsonByteClass } from "./jsonByteClass.ts";
import type { Ninja } from "./ninja.ts";
import { quote, quoteArgs } from "./shell.ts";
import { generateXmlByteClass } from "./xmlByteClass.ts";

// The individual emit functions take these four params. Bundled to keep
// signatures short.
Expand Down Expand Up @@ -290,6 +291,10 @@ export function emitCodegen(n: Ninja, cfg: Config, sources: Sources): CodegenOut
o.all.push(jsonByteClass.h, jsonByteClass.rs);
o.rustInputs.push(jsonByteClass.rs);
o.cppHeaders.push(jsonByteClass.h);
const xmlByteClass = generateXmlByteClass(cfg);
o.all.push(xmlByteClass.h, xmlByteClass.rs);
o.rustInputs.push(xmlByteClass.rs);
o.cppHeaders.push(xmlByteClass.h);

emitBunError(ctx);
emitStringMaps(ctx);
Expand Down
6 changes: 6 additions & 0 deletions scripts/build/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,12 @@ export const fileOverrides: FileOverride[] = [
when: c => c.windows,
desc: "Vendored electron/rcedit; VersionInfo ctor throws std::system_error caught in OnEnumResourceLanguage. Self-contained throw/catch — already excluded from PCH",
},
{
file: "src/jsc/bindings/highway_xml.cpp",
extraFlags: ["-O2"],
when: c => c.debug,
desc: "Same as highway_json.cpp below: the XML structural-index kernel must be optimized even in debug builds.",
},
{
file: "src/jsc/bindings/highway_json.cpp",
extraFlags: ["-O2"],
Expand Down
2 changes: 2 additions & 0 deletions scripts/build/unified.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ const noUnify: readonly string[] = [
// Fifth highway TU (JSON structural indexer) — same foreach_target.h
// include-guard reason.
"src/jsc/bindings/highway_json.cpp",
// Sixth highway TU (XML structural indexer) — same reason.
"src/jsc/bindings/highway_xml.cpp",
// Declares its own minimal CGRect/kCFStringEncodingUTF8/kCFNumberDoubleType
// so it doesn't pull a CoreGraphics load command; bundled with files that
// include the real CF headers those names become ambiguous.
Expand Down
106 changes: 106 additions & 0 deletions scripts/build/xmlByteClass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Generates `xml_byte_class.h` (nibble LUTs, Highway SIMD kernel) and `xml_byte_class.rs` (the
// derived 256-entry table, Rust scalar indexer): both come from this one table so they agree.

Comment thread
claude[bot] marked this conversation as resolved.
import { mkdirSync } from "node:fs";
import { resolve } from "node:path";
import type { Config } from "./config.ts";
import { writeIfChanged } from "./fs.ts";

// Indexed everywhere: `&`, `\r`, and the control characters XML forbids.
const ALWAYS = 0x07;
// Indexed only between `<` and the next `>`: `\t`, `\n`, `"`, `'`, `=`.
const TAG = 0x38;
const LT = 0x40;
const GT = 0x80;

const LUT_LO = [0x03, 0x03, 0x13, 0x03, 0x03, 0x03, 0x07, 0x13, 0x03, 0x0a, 0x0a, 0x03, 0x43, 0x23, 0x83, 0x03];
const LUT_HI = [0x09, 0x02, 0x14, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];

const classOf = (b: number): number => LUT_LO[b & 0xf] & LUT_HI[b >> 4];

function check() {
const expectedClass = (b: number): number => {
const ch = String.fromCharCode(b);
if (ch === "<") return LT;
if (ch === ">") return GT;
if (ch === "&" || ch === "\r") return ALWAYS;
if (b < 0x20 && ch !== "\t" && ch !== "\n") return ALWAYS;
if ("\t\n\"'=".includes(ch)) return TAG;
return 0;
};
for (let b = 0; b < 0x100; b++) {
const got = classOf(b);
const expected = b < 0x80 ? expectedClass(b) : 0;
const ok = expected === 0 ? got === 0 : (got & expected) !== 0 && (got & ~expected) === 0;
if (!ok) {
throw new Error(
`xml_byte_class: byte 0x${b.toString(16)} classifies as 0x${got.toString(16)}, expected class 0x${expected.toString(16)}`,
);
}
}
}

export function generateXmlByteClass(cfg: Config): { h: string; rs: string } {
check();

const banner = (comment: string) => [
`${comment} Generated by scripts/build/xmlByteClass.ts at configure time. Do not`,
`${comment} edit. The same definition feeds the Highway XML kernel (these nibble`,
`${comment} LUTs) and the Rust scalar indexer (the derived 256-entry table).`,
`${comment}`,
`${comment} cls = LUT_LO[b & 0xF] & LUT_HI[b >> 4]`,
`${comment} 0x07 always & \\r control 0x38 tag \\t \\n " ' =`,
`${comment} 0x40 < 0x80 >`,
"",
];
const hex = (v: number) => `0x${v.toString(16).padStart(2, "0")}`;

const h = [
...banner("//"),
"#pragma once",
"#include <stdint.h>",
"",
`#define BUN_XML_CLASS_ALWAYS ${hex(ALWAYS)}`,
`#define BUN_XML_CLASS_TAG ${hex(TAG)}`,
`#define BUN_XML_CLASS_LT ${hex(LT)}`,
`#define BUN_XML_CLASS_GT ${hex(GT)}`,
"",
`alignas(16) static const uint8_t kBunXmlLutLo[16] = { ${LUT_LO.map(hex).join(", ")} };`,
`alignas(16) static const uint8_t kBunXmlLutHi[16] = { ${LUT_HI.map(hex).join(", ")} };`,
"",
].join("\n");

const table: string[] = [];
for (let row = 0; row < 256; row += 16) {
const cells = [];
for (let b = row; b < row + 16; b++) cells.push(hex(classOf(b)));
table.push(` ${cells.join(", ")},`);
}
const allow = "#[allow(dead_code, unreachable_pub, unused)]";
const rs = [
...banner("//"),
allow,
`pub const CLASS_ALWAYS: u8 = ${hex(ALWAYS)};`,
allow,
`pub const CLASS_TAG: u8 = ${hex(TAG)};`,
allow,
`pub const CLASS_LT: u8 = ${hex(LT)};`,
allow,
`pub const CLASS_GT: u8 = ${hex(GT)};`,
"",
"/// `LUT_LO[b & 0xF] & LUT_HI[b >> 4]` for every byte `b`.",
allow,
"#[rustfmt::skip]",
"pub const XML_BYTE_CLASS: [u8; 256] = [",
...table,
"];",
"",
].join("\n");

mkdirSync(cfg.codegenDir, { recursive: true });
const hPath = resolve(cfg.codegenDir, "xml_byte_class.h");
const rsPath = resolve(cfg.codegenDir, "xml_byte_class.rs");
writeIfChanged(hPath, h);
writeIfChanged(rsPath, rs);
return { h: hPath, rs: rsPath };
}
1 change: 1 addition & 0 deletions scripts/verify-baseline-static/allowlist-aarch64.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ _ZN3bun10N_SVE2_12811MemRMemImplEPKhmS2_m [S
_ZN3bun10N_SVE2_12812MemMem16ImplEPKtmS2_m [SVE]
_ZN3bun10N_SVE2_12813MemRMem16ImplEPKtmS2_m [SVE]
_ZN3bun10N_SVE2_12813JsonIndexImplEPKhmmPjPmS4_S3_ [SVE]
_ZN3bun10N_SVE2_12812XmlIndexImplEPKhmmPjPm [SVE]
_ZN3bun10N_SVE2_12814DecodeHex8ImplEPKhPhm [SVE]
_ZN3bun10N_SVE2_12814LowerAsciiImplEPKhmPh [SVE]
_ZN3bun10N_SVE2_12815CopyU16ToU8ImplEPKtmPh [SVE]
Expand Down
Loading
Loading