From 053410f5839373b437021927ab452045fc66554b Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 26 Jun 2026 12:49:11 -0400 Subject: [PATCH 1/2] benchmarks: cross-runtime for node/bun/deno --- .gitattributes | 9 +- .github/workflows/check.yml | 2 +- .gitignore | 18 +- CLAUDE.md | 74 +- Cargo.lock | 166 +++ Cargo.toml | 8 + benches/deno/deno.json | 25 - benches/deno/lib/versions.ts | 90 -- benches/deno/results/report.json | 684 ---------- benches/deno/results/report.md | 193 --- benches/{deno => js}/CLAUDE.md | 314 +++-- benches/{deno => js}/bench.ts | 166 ++- benches/js/compose_reports.ts | 185 +++ benches/{deno => js}/corpus_compare_format.ts | 0 benches/{deno => js}/corpus_compare_parse.ts | 0 benches/js/deno.json | 13 + benches/{deno => js}/deno.lock | 33 +- .../diagnostics/acorn_dup_fuzz.ts | 2 +- .../diagnostics/comment_dup_scan.ts | 2 +- .../{deno => js}/diagnostics/skip_triage.ts | 2 +- .../diagnostics/test262_compare.ts | 4 +- .../diagnostics/wasm_format_probe.ts | 4 +- .../diagnostics/wasm_json_probe.ts | 2 +- benches/{deno => js}/divergence_audit.ts | 0 benches/js/install_deps.ts | 43 + benches/{deno => js}/lib/binary_sizes.ts | 161 ++- benches/{deno => js}/lib/biome.ts | 17 +- benches/{deno => js}/lib/canonical.ts | 0 benches/{deno => js}/lib/canonical_test.ts | 0 .../lib/check_artifact_freshness.ts | 43 +- benches/{deno => js}/lib/compare_cli.ts | 0 benches/{deno => js}/lib/corpus.ts | 42 +- benches/{deno => js}/lib/diff.ts | 0 .../lib/divergence/expected_errors.ts | 0 .../lib/divergence/expected_errors_test.ts | 0 .../lib/divergence/fixture_coverage_test.ts | 0 benches/{deno => js}/lib/divergence/mod.ts | 0 .../{deno => js}/lib/divergence/patterns.ts | 0 .../lib/divergence/patterns_test.ts | 0 benches/{deno => js}/lib/divergence/safety.ts | 0 .../lib/divergence/safety_test.ts | 0 .../{deno => js}/lib/divergence/validation.ts | 0 benches/{deno => js}/lib/ffi.ts | 0 benches/{deno => js}/lib/implementations.ts | 24 +- benches/js/lib/napi.ts | 133 ++ benches/{deno => js}/lib/oxc.ts | 2 +- benches/{deno => js}/lib/oxc_wasm.ts | 16 +- benches/{deno => js}/lib/report.ts | 0 benches/js/lib/runtime.ts | 58 + benches/{deno => js}/lib/types.ts | 6 +- benches/js/lib/versions.ts | 96 ++ benches/{deno => js}/lib/wasm.ts | 42 +- benches/js/package-lock.json | 1188 +++++++++++++++++ benches/js/package.json | 26 + benches/js/results/report.bun.json | 648 +++++++++ benches/js/results/report.bun.md | 191 +++ benches/js/results/report.deno.json | 736 ++++++++++ benches/js/results/report.deno.md | 197 +++ benches/js/results/report.json | 628 +++++++++ benches/js/results/report.md | 69 + benches/js/results/report.node.json | 736 ++++++++++ benches/js/results/report.node.md | 197 +++ benches/{deno => js}/smoke.ts | 51 +- crates/tsv_debug/CLAUDE.md | 2 +- .../src/cli/commands/json_profile.rs | 2 +- crates/tsv_debug/src/cli/commands/test262.rs | 2 +- crates/tsv_debug/src/deno/sidecar.ts | 6 +- crates/tsv_debug/src/test262/runner.rs | 2 +- crates/tsv_ffi/CLAUDE.md | 2 +- crates/tsv_napi/CLAUDE.md | 49 + crates/tsv_napi/Cargo.toml | 109 ++ crates/tsv_napi/build.rs | 7 + crates/tsv_napi/src/lib.rs | 187 +++ deno.json | 31 +- docs/cli.md | 2 +- docs/conformance_prettier.md | 2 +- docs/conformance_svelte.md | 6 +- docs/conformance_test262.md | 4 +- docs/divergence_detector.md | 6 +- docs/fixture_overview.md | 2 +- docs/performance.md | 13 +- docs/workflow_corpus.md | 4 +- 82 files changed, 6396 insertions(+), 1388 deletions(-) delete mode 100644 benches/deno/deno.json delete mode 100644 benches/deno/lib/versions.ts delete mode 100644 benches/deno/results/report.json delete mode 100644 benches/deno/results/report.md rename benches/{deno => js}/CLAUDE.md (74%) rename benches/{deno => js}/bench.ts (88%) create mode 100644 benches/js/compose_reports.ts rename benches/{deno => js}/corpus_compare_format.ts (100%) rename benches/{deno => js}/corpus_compare_parse.ts (100%) create mode 100644 benches/js/deno.json rename benches/{deno => js}/deno.lock (97%) rename benches/{deno => js}/diagnostics/acorn_dup_fuzz.ts (97%) rename benches/{deno => js}/diagnostics/comment_dup_scan.ts (99%) rename benches/{deno => js}/diagnostics/skip_triage.ts (97%) rename benches/{deno => js}/diagnostics/test262_compare.ts (98%) rename benches/{deno => js}/diagnostics/wasm_format_probe.ts (98%) rename benches/{deno => js}/diagnostics/wasm_json_probe.ts (98%) rename benches/{deno => js}/divergence_audit.ts (100%) create mode 100644 benches/js/install_deps.ts rename benches/{deno => js}/lib/binary_sizes.ts (72%) rename benches/{deno => js}/lib/biome.ts (80%) rename benches/{deno => js}/lib/canonical.ts (100%) rename benches/{deno => js}/lib/canonical_test.ts (100%) rename benches/{deno => js}/lib/check_artifact_freshness.ts (82%) rename benches/{deno => js}/lib/compare_cli.ts (100%) rename benches/{deno => js}/lib/corpus.ts (85%) rename benches/{deno => js}/lib/diff.ts (100%) rename benches/{deno => js}/lib/divergence/expected_errors.ts (100%) rename benches/{deno => js}/lib/divergence/expected_errors_test.ts (100%) rename benches/{deno => js}/lib/divergence/fixture_coverage_test.ts (100%) rename benches/{deno => js}/lib/divergence/mod.ts (100%) rename benches/{deno => js}/lib/divergence/patterns.ts (100%) rename benches/{deno => js}/lib/divergence/patterns_test.ts (100%) rename benches/{deno => js}/lib/divergence/safety.ts (100%) rename benches/{deno => js}/lib/divergence/safety_test.ts (100%) rename benches/{deno => js}/lib/divergence/validation.ts (100%) rename benches/{deno => js}/lib/ffi.ts (100%) rename benches/{deno => js}/lib/implementations.ts (93%) create mode 100644 benches/js/lib/napi.ts rename benches/{deno => js}/lib/oxc.ts (97%) rename benches/{deno => js}/lib/oxc_wasm.ts (77%) rename benches/{deno => js}/lib/report.ts (100%) create mode 100644 benches/js/lib/runtime.ts rename benches/{deno => js}/lib/types.ts (90%) create mode 100644 benches/js/lib/versions.ts rename benches/{deno => js}/lib/wasm.ts (68%) create mode 100644 benches/js/package-lock.json create mode 100644 benches/js/package.json create mode 100644 benches/js/results/report.bun.json create mode 100644 benches/js/results/report.bun.md create mode 100644 benches/js/results/report.deno.json create mode 100644 benches/js/results/report.deno.md create mode 100644 benches/js/results/report.json create mode 100644 benches/js/results/report.md create mode 100644 benches/js/results/report.node.json create mode 100644 benches/js/results/report.node.md rename benches/{deno => js}/smoke.ts (77%) create mode 100644 crates/tsv_napi/CLAUDE.md create mode 100644 crates/tsv_napi/Cargo.toml create mode 100644 crates/tsv_napi/build.rs create mode 100644 crates/tsv_napi/src/lib.rs diff --git a/.gitattributes b/.gitattributes index 5d4986373..8632b1bcc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,9 @@ tests/fixtures/**/expected*.json linguist-generated tests/fixtures/**/audit_signature.txt linguist-generated -# Committed benchmark reports (regenerated by `deno task bench`) -benches/deno/results/report.json linguist-generated -benches/deno/results/report.md linguist-generated +# Committed benchmark reports (regenerated by `deno task bench`): the per-runtime +# siblings (report..{json,md}) and the combined cross-runtime report.{json,md}. +benches/js/results/report.json linguist-generated +benches/js/results/report.md linguist-generated +benches/js/results/report.*.json linguist-generated +benches/js/results/report.*.md linguist-generated diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6527e76e7..6ddb4a2fc 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -69,7 +69,7 @@ jobs: # npm fetches (prettier/svelte/acorn) since those land in DENO_DIR. cache: true # Key on the sidecar pins (fetched at runtime, not lockfile-pinned) so a - # bump invalidates the cached deps. NOT benches/deno/deno.lock: it governs + # bump invalidates the cached deps. NOT benches/js/deno.lock: it governs # `test:deno` but is dominated by bench-only tools (biome/oxc/oxfmt) that # `check` never fetches, so keying on it would bust the cache on unrelated # bumps. (There is no root deno.lock — the root config has no external deps.) diff --git a/.gitignore b/.gitignore index 02bd053f6..71c6b759f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,11 +20,19 @@ node_modules # Drift-check generated file (left on failure for inspection) /scripts/.drift_check.gen.ts -# Benchmark results (report.json and report.md are committed; baseline.json lives here too) -/benches/deno/results/*.json -/benches/deno/results/*.md -!/benches/deno/results/report.json -!/benches/deno/results/report.md +# Benchmark results (the per-runtime report..{json,md} siblings and the +# combined report.{json,md} are committed; timestamped runs + baseline.json are +# gitignored) +/benches/js/results/*.json +/benches/js/results/*.md +!/benches/js/results/report.deno.json +!/benches/js/results/report.deno.md +!/benches/js/results/report.node.json +!/benches/js/results/report.node.md +!/benches/js/results/report.bun.json +!/benches/js/results/report.bun.md +!/benches/js/results/report.json +!/benches/js/results/report.md # Env .env* diff --git a/CLAUDE.md b/CLAUDE.md index 52415d493..d45116373 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -192,12 +192,11 @@ See ./docs/fixture_workflow.md. Use `--prettier-only` with `fixtures:validate` d ### JS Bindings -Two binding crates for different use cases: +Three binding crates for different use cases: - `tsv_ffi` (C ABI) — target: Any FFI (Deno, Python, etc.); output: `libtsv_ffi.so` / `.dylib` / `.dll` - `tsv_wasm` (wasm-bindgen) — target: Browser, Deno, Node; output: `.wasm` module (format / parse / all variants via cargo features) - -N-API is a maybe — the decision is deferred; there is currently no `tsv_napi` crate. +- `tsv_napi` (napi-rs) — target: Node.js / Bun native addon; output: `libtsv_napi.{so,dylib,dll}` (loaded via `process.dlopen`). Currently a **measurement-only** binding for the Node benchmark runner (single-platform local build, `deno task build:napi`); the cross-platform publish matrix as `@fuzdev/tsv_napi` is targeted for 0.2. tsv-scoped carve-out from the ecosystem N-API deferral. See ./crates/tsv_napi/CLAUDE.md. `tsv_wasm` produces three npm packages from one crate via the `format` + `parse` cargo features (default = both): `@fuzdev/tsv_format_wasm` (format only), `@fuzdev/tsv_parse_wasm` (parse only), and `@fuzdev/tsv_wasm` (everything + the `tsv` CLI). Each variant has its own output directory. @@ -268,37 +267,59 @@ deno task divergence:audit # audit divergence pattern coverage (--json f The corpus comparison builds with `--profile corpus` (release + `panic = "unwind"`) so panics in our code are caught and reported as errors instead of crashing the process. Benchmarks use `--release` (with `panic = "abort"`) for maximum performance. -Divergence detection identifies known differences documented in `conformance_prettier.md` (safety checks, pattern detection, traceability). See ./benches/deno/CLAUDE.md and ./docs/divergence_detector.md. +Divergence detection identifies known differences documented in `conformance_prettier.md` (safety checks, pattern detection, traceability). See ./benches/js/CLAUDE.md and ./docs/divergence_detector.md. ### Benchmarks +**Cross-runtime.** The same harness runs under **Deno, Node, and Bun** — each emits its +own runtime-labeled sibling report (`report.{deno,node,bun}.{json,md}`), never merged; +`deno task bench:compose` folds them into a compact combined `report.{json,md}` (the +cross-runtime view tsv.fuz.dev consumes). The native row differs by runtime: Deno loads the +**FFI** library via `Deno.dlopen`, Node/Bun load the **N-API** addon (`tsv_napi`) via +`process.dlopen`. Everything else (corpus, registry, timing, report) is runtime-neutral +shared code using `node:` builtins. + ```bash -# Smoke test (fast sanity check that every formatter+parser produces output) +# One-time: install the harness's npm deps (package.json is the source of truth; +# both runtimes consume the same node_modules). Re-run after a dep bump or a plain +# `npm install` (which prunes the oxc-parser-wasm binding — see benches/js/CLAUDE.md). +deno task bench:install + +# Smoke test (Deno; fast sanity check that every formatter+parser produces output) deno task smoke -# Run benchmarks (builds the bench artifact set — `build:bench` — automatically) -deno task bench +# Run benchmarks (builds the runtime's bench artifacts automatically) +deno task bench # ALL three + compose → report.{deno,node,bun}.* + combined report.{json,md} +deno task bench:deno # Deno only +deno task bench:node # Node only +deno task bench:bun # Bun only (reuses the Node artifacts) +deno task bench:compose # Fold existing per-runtime reports → combined report.{json,md} -# Run without rebuilding (if already built). Aborts if the FFI/WASM artifacts -# are older than crate source — rebuild, or set BENCH_STALE_OK=1 to override -deno task bench:run +# Run without rebuilding (if already built; aborts on stale artifacts) +deno task bench:deno:run +deno task bench:node:run +deno task bench:bun:run # Per-file skip detail (off by default — counts always shown, paths/errors opt-in) -deno task bench:run -- --verbose # Include per-file skip detail in report +deno task bench:deno:run -- --verbose -# Environment variables -BENCH_LIMIT=10 deno task bench:run # Limit files per language (default: all) -BENCH_FILTER=zzz deno task bench:run # Filter by path pattern (default: none) -BENCH_DURATION=10000 deno task bench:run # Duration per benchmark in ms (default: 5000) -BENCH_WARMUP=10 deno task bench:run # Set warmup iterations (default: 3) -BENCH_MODE=union deno task bench:run # Per-impl iteration (default: intersection) -BENCH_STALE_OK=1 deno task bench:run # Run despite stale artifacts (default: off) -BENCH_FORCED_ASYNC=1 deno task bench:run # Add tsv-forced-async control row (diagnostic; default: off) +# Environment variables (apply to any runtime) +BENCH_LIMIT=10 deno task bench:deno:run # Limit files per language (default: all) +BENCH_FILTER=zzz deno task bench:deno:run # Filter by path pattern (default: none) +BENCH_DURATION=10000 deno task bench:deno:run # Duration per benchmark in ms (default: 5000) +BENCH_WARMUP=10 deno task bench:deno:run # Set warmup iterations (default: 3) +BENCH_MODE=union deno task bench:deno:run # Per-impl iteration (default: intersection) +BENCH_STALE_OK=1 deno task bench:deno:run # Run despite stale artifacts (default: off) +BENCH_FORCED_ASYNC=1 deno task bench:deno:run # Add tsv-forced-async control row (diagnostic; default: off) ``` -**Prerequisites**: `cargo install wasm-pack` +**Prerequisites**: `cargo install wasm-pack`; Node ≥ 22.18 (native TS type-stripping); Bun (for `bench:bun`); `deno task bench:install` once. -Compares three implementations: canonical (prettier + svelte/compiler), native (FFI), WASM. Results are saved to `benches/deno/results/report.{json,md}` (committed). To publish to tsv.fuz.dev: `npm run update-benchmarks` in ~/dev/tsv.fuz.dev. See ./benches/deno/CLAUDE.md. +Compares: canonical (prettier + svelte/compiler), native (FFI under Deno / N-API under Node +and Bun), WASM, and alternatives (oxc-parser, oxfmt, biome-wasm). Each runtime's results save +to `benches/js/results/report..{json,md}` (committed; every row carries a `runtime` +field), plus the combined `report.{json,md}`. To publish to tsv.fuz.dev: `npm run update-benchmarks` in ~/dev/tsv.fuz.dev. See +./benches/js/CLAUDE.md. ### Performance Profiling @@ -379,8 +400,9 @@ tsv/ │ ├── tsv_svelte/ # Svelte: parse(), format(), convert_ast() │ ├── tsv_cli/ # Production CLI (binary: tsv) - pure Rust │ ├── tsv_debug/ # Dev utilities (binary: tsv_debug) - uses Deno -│ ├── tsv_ffi/ # C FFI bindings -│ └── tsv_wasm/ # WebAssembly bindings (published as @fuzdev/tsv_format_wasm + @fuzdev/tsv_parse_wasm + @fuzdev/tsv_wasm; bundles hand-maintained types/tsv_ast.d.ts; npm/cli.js is the tsv bin) +│ ├── tsv_ffi/ # C FFI bindings (Deno's native path) +│ ├── tsv_wasm/ # WebAssembly bindings (published as @fuzdev/tsv_format_wasm + @fuzdev/tsv_parse_wasm + @fuzdev/tsv_wasm; bundles hand-maintained types/tsv_ast.d.ts; npm/cli.js is the tsv bin) +│ └── tsv_napi/ # N-API bindings (Node/Bun native path; measurement-only for the Node bench, 0.2 publish target) ├── scripts/ # Publish orchestrator, npm package patcher, Node artifact tests, AST type drift check ├── tests/ # Integration tests (parser, formatter, CLI) │ └── fixtures/ # Test fixtures organized by language/feature @@ -659,8 +681,8 @@ cargo run -p tsv_debug test262 language/expressions # filter by path pattern # triage tsv's failures (real bug vs shared limitation). See ./docs/conformance_test262.md §Differential. cargo run -p tsv_debug test262 --emit-manifest /tmp/t262.json # path/expected/tsv verdict per graded test deno run --allow-read --allow-env --allow-ffi --allow-net --allow-sys \ - --config benches/deno/deno.json \ - benches/deno/diagnostics/test262_compare.ts --manifest /tmp/t262.json + --config benches/js/deno.json \ + benches/js/diagnostics/test262_compare.ts --manifest /tmp/t262.json ``` See ./docs/conformance_test262.md. @@ -883,6 +905,8 @@ Higher-fidelity models (attached comments, trivia tokens) may be needed for IDE/ - `unicode-ident` — Unicode XID_Start/XID_Continue for identifiers - `unicode-segmentation` — Grapheme clustering for visual width measurement - `unicode-width` — Character display width (CJK, zero-width) +- `bumpalo` — Bump arena for the internal AST (and the binding crates' per-thread `reset()` reuse) +- `napi` / `napi-derive` / `napi-build` — N-API bindings for `tsv_napi` (Node/Bun native addon; tsv-scoped carve-out) ## Canonical References diff --git a/Cargo.lock b/Cargo.lock index 1fd108978..4c1e7eafb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "ctor" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a" + [[package]] name = "equivalent" version = "1.0.2" @@ -85,12 +100,54 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + [[package]] name = "futures-macro" version = "0.3.31" @@ -102,6 +159,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + [[package]] name = "futures-task" version = "0.3.31" @@ -114,9 +177,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -175,6 +242,16 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -198,6 +275,69 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "napi" +version = "3.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41bda2ac390efb5e8d22025d925ccc3f3807d8c1bea6d19b36127247c4b8f83" +dependencies = [ + "bitflags", + "ctor", + "futures", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", +] + +[[package]] +name = "napi-build" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" + +[[package]] +name = "napi-derive" +version = "3.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61d66f70256ad5aef58659966064471d0ad90e2897bc36a5a5e0389c85aabc1e" +dependencies = [ + "convert_case", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "5.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b4b08f15eed7a2a20c3f4c6314013fc3ac890a3afa9892b594485299ebdb2d" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f5bcdf71abd3a50d00b49c1c2c75251cb3c913777d6139cd37dabc093a5e400" +dependencies = [ + "libloading", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "once_cell" version = "1.21.3" @@ -283,6 +423,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustix" version = "1.1.2" @@ -308,6 +454,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -571,6 +723,20 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "tsv_napi" +version = "0.1.0" +dependencies = [ + "bumpalo", + "napi", + "napi-build", + "napi-derive", + "serde_json", + "tsv_css", + "tsv_svelte", + "tsv_ts", +] + [[package]] name = "tsv_svelte" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 69ab8e93c..491925e98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/tsv_debug", "crates/tsv_wasm", "crates/tsv_ffi", + "crates/tsv_napi", ] [workspace.package] @@ -38,6 +39,13 @@ tsv_svelte = { path = "crates/tsv_svelte", default-features = false } # External dependencies argh = "0.1" +# N-API (Node.js/Bun) bindings — the native path for the `tsv_napi` crate, the +# Node-runtime sibling of `tsv_ffi` (Deno's C-FFI path). napi/napi-derive ride +# the 3.x line together; napi-build (build dep) is its own 2.x line. tsv-scoped +# carve-out from the ecosystem N-API deferral — see lore TODO_NAPI_BINDINGS.md. +napi = "3.9.4" +napi-derive = "3.5.7" +napi-build = "2.3.2" # Bump arena for the internal AST: per-parse `Bump`, nodes/collections/decoded # strings allocated as `&'arena ...` (no per-node heap alloc/free, better # traversal locality). Already present transitively (via wasm-bindgen); this diff --git a/benches/deno/deno.json b/benches/deno/deno.json deleted file mode 100644 index 29bde733b..000000000 --- a/benches/deno/deno.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "//canonical-sync": "⚠ The 5 canonical packages (prettier, svelte, acorn, @sveltejs/acorn-typescript, prettier-plugin-svelte) are ALSO pinned as literals in crates/tsv_debug/src/deno/sidecar.ts and MUST match. Bumping any re-baselines the whole fixture corpus — see benches/deno/CLAUDE.md §'Canonical baseline is coupled'. The other imports (oxc, oxfmt, biome, etc.) are free to bump independently.", - "imports": { - "acorn": "npm:acorn@8.16.0", - "svelte": "npm:svelte@5.56.1", - "svelte/compiler": "npm:svelte@5.56.1/compiler", - "@sveltejs/acorn-typescript": "npm:@sveltejs/acorn-typescript@1.0.10", - "prettier": "npm:prettier@3.8.3", - "prettier-plugin-svelte": "npm:prettier-plugin-svelte@4.1.1", - "@biomejs/js-api": "npm:@biomejs/js-api@4.0.0", - "@biomejs/wasm-bundler": "npm:@biomejs/wasm-bundler@2.4.16", - "oxc-parser": "npm:oxc-parser@0.134.0", - "@oxc-parser/binding-wasm32-wasi/parser.wasi-browser.js": "npm:@oxc-parser/binding-wasm32-wasi@0.134.0/parser.wasi-browser.js", - "oxfmt": "npm:oxfmt@0.53.0", - "zod": "npm:zod@^4.4.3", - "@fuzdev/fuz_util": "npm:@fuzdev/fuz_util@0.64.0", - "@std/fs": "jsr:@std/fs@^1.0.24" - }, - "fmt": { - "useTabs": true, - "lineWidth": 100, - "singleQuote": true, - "proseWrap": "preserve" - } -} diff --git a/benches/deno/lib/versions.ts b/benches/deno/lib/versions.ts deleted file mode 100644 index 33138590f..000000000 --- a/benches/deno/lib/versions.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Centralized version loading from deno.json - * - * Single source of truth for all package versions used in benchmarks. - */ - -import { extract_version } from './types.ts'; - -/** Canonical implementation versions */ -export interface CanonicalVersions { - prettier: string; - 'prettier-plugin-svelte': string; - svelte: string; - acorn: string; - '@sveltejs/acorn-typescript': string; -} - -/** OXC implementation versions */ -export interface OxcVersions { - 'oxc-parser': string; - oxfmt: string; -} - -/** Biome implementation versions */ -export interface BiomeVersions { - js_api: string; - wasm: string; -} - -/** All implementation versions */ -export interface AllVersions { - canonical: CanonicalVersions; - oxc: OxcVersions; - biome: BiomeVersions; -} - -/** Default versions when loading fails */ -const DEFAULT_VERSIONS: AllVersions = { - canonical: { - prettier: 'unknown', - 'prettier-plugin-svelte': 'unknown', - svelte: 'unknown', - acorn: 'unknown', - '@sveltejs/acorn-typescript': 'unknown', - }, - oxc: { - 'oxc-parser': 'unknown', - oxfmt: 'unknown', - }, - biome: { - js_api: 'unknown', - wasm: 'unknown', - }, -}; - -/** - * Load all package versions from deno.json import map. - * - * Reads benches/deno/deno.json to extract versions for all implementations. - */ -export async function load_all_versions(): Promise { - try { - const deno_json_path = new URL('../deno.json', import.meta.url).pathname; - const content = await Deno.readTextFile(deno_json_path); - const config = JSON.parse(content); - const imports = config.imports || {}; - - return { - canonical: { - prettier: extract_version(imports['prettier'] || ''), - 'prettier-plugin-svelte': extract_version(imports['prettier-plugin-svelte'] || ''), - svelte: extract_version(imports['svelte'] || ''), - acorn: extract_version(imports['acorn'] || ''), - '@sveltejs/acorn-typescript': extract_version( - imports['@sveltejs/acorn-typescript'] || '', - ), - }, - oxc: { - 'oxc-parser': extract_version(imports['oxc-parser'] || ''), - oxfmt: extract_version(imports['oxfmt'] || ''), - }, - biome: { - js_api: extract_version(imports['@biomejs/js-api'] || ''), - wasm: extract_version(imports['@biomejs/wasm-bundler'] || ''), - }, - }; - } catch { - return DEFAULT_VERSIONS; - } -} diff --git a/benches/deno/results/report.json b/benches/deno/results/report.json deleted file mode 100644 index 13d4ef4af..000000000 --- a/benches/deno/results/report.json +++ /dev/null @@ -1,684 +0,0 @@ -{ - "version": 4, - "timestamp": "2026-06-12T09:22:00.588Z", - "git_commit": "9b5c4e98", - "corpus": { - "svelte": 1240, - "typescript": 4025, - "css": 193 - }, - "versions": { - "svelte": "5.56.1", - "acorn": "8.16.0", - "acorn_ts": "1.0.10", - "prettier": "3.8.3", - "prettier_svelte": "3.5.2", - "oxc_parser": "0.134.0", - "oxfmt": "0.53.0", - "biome": "2.4.16" - }, - "binary_sizes": [ - { - "label": "tsv (native)", - "bytes": 3861760, - "kind": "native", - "gzip_bytes": 1622771 - }, - { - "label": "tsv_format_wasm", - "bytes": 2196292, - "kind": "wasm", - "gzip_bytes": 715422 - }, - { - "label": "tsv_parse_wasm", - "bytes": 1699625, - "kind": "wasm", - "gzip_bytes": 526578 - }, - { - "label": "tsv_wasm", - "bytes": 2888918, - "kind": "wasm", - "gzip_bytes": 911323 - }, - { - "label": "biome (wasm)", - "bytes": 34430368, - "kind": "wasm", - "gzip_bytes": 8187702 - }, - { - "label": "oxc-parser (native)", - "bytes": 2658008, - "kind": "native", - "gzip_bytes": 1032774 - }, - { - "label": "oxfmt (native)", - "bytes": 8016464, - "kind": "native", - "gzip_bytes": 3224684 - }, - { - "label": "oxc-parser (wasm)", - "bytes": 1866798, - "kind": "wasm", - "gzip_bytes": 518682 - } - ], - "entries": [ - { - "name": "svelte/compiler", - "group": "parse/svelte", - "mean_ns": 353647929.8666667, - "p50_ns": 351794280, - "p75_ns": 355710613.5, - "p90_ns": 358757843.8, - "p95_ns": 359832344.1, - "p99_ns": 361455435.21999997, - "min_ns": 349590498, - "max_ns": 361861208, - "std_dev_ns": 3732468.9277879535, - "cv": 0.010554194193064213, - "ops_per_second": 2.8276710127414653, - "sample_size": 15, - "files_processed": 1193, - "files_total": 1240, - "files_iterated": 1192 - }, - { - "name": "tsv-json", - "group": "parse/svelte", - "mean_ns": 485025992.4444444, - "p50_ns": 485086813, - "p75_ns": 487644066, - "p90_ns": 491656222, - "p95_ns": 494258617.5, - "p99_ns": 496340533.9, - "min_ns": 481783564, - "max_ns": 496861013, - "std_dev_ns": 1892163.1540198317, - "cv": 0.0039011582543930627, - "ops_per_second": 2.061745175676418, - "sample_size": 9, - "files_processed": 1202, - "files_total": 1240, - "files_iterated": 1192 - }, - { - "name": "tsv_wasm-json", - "group": "parse/svelte", - "mean_ns": 515988848.71428573, - "p50_ns": 516072939.5, - "p75_ns": 517319991, - "p90_ns": 519264292.5, - "p95_ns": 520772636.25, - "p99_ns": 521979311.25, - "min_ns": 515560589, - "max_ns": 522280980, - "std_dev_ns": 253147.57598596797, - "cv": 0.0004906066800023837, - "ops_per_second": 1.9380263788485899, - "sample_size": 7, - "files_processed": 1202, - "files_total": 1240, - "files_iterated": 1192 - }, - { - "name": "tsv-internal", - "group": "parse/svelte", - "mean_ns": 37041735.949579835, - "p50_ns": 36817941, - "p75_ns": 38031003.5, - "p90_ns": 39745595, - "p95_ns": 49374707.5, - "p99_ns": 70563356.69999997, - "min_ns": 35490755, - "max_ns": 73560234, - "std_dev_ns": 1041838.039960544, - "cv": 0.02812605870790355, - "ops_per_second": 26.996574927297463, - "sample_size": 119, - "files_processed": 1202, - "files_total": 1240, - "files_iterated": 1192 - }, - { - "name": "tsv_wasm-internal", - "group": "parse/svelte", - "mean_ns": 45254932.2745098, - "p50_ns": 45241227, - "p75_ns": 45348407.5, - "p90_ns": 45449227, - "p95_ns": 45624719, - "p99_ns": 45694463.7, - "min_ns": 45087137, - "max_ns": 45720491, - "std_dev_ns": 94029.59204496059, - "cv": 0.0020777755554817943, - "ops_per_second": 22.097038924600444, - "sample_size": 102, - "files_processed": 1202, - "files_total": 1240, - "files_iterated": 1192 - }, - { - "name": "prettier", - "group": "format/svelte", - "mean_ns": 3156053205.2, - "p50_ns": 3153333425, - "p75_ns": 3161183958, - "p90_ns": 3191384359.8, - "p95_ns": 3201451160.4, - "p99_ns": 3209504600.88, - "min_ns": 3116677318, - "max_ns": 3211517961, - "std_dev_ns": 35365115.51584766, - "cv": 0.011205487745763958, - "ops_per_second": 0.316851439117811, - "sample_size": 5, - "files_processed": 1201, - "files_total": 1240, - "files_iterated": 1194 - }, - { - "name": "tsv", - "group": "format/svelte", - "mean_ns": 253589550.66666666, - "p50_ns": 250899714, - "p75_ns": 261502311.75, - "p90_ns": 270307677.1, - "p95_ns": 291692469.25, - "p99_ns": 299630277.84999996, - "min_ns": 247143299, - "max_ns": 301614730, - "std_dev_ns": 6469364.524097994, - "cv": 0.02551116363860637, - "ops_per_second": 3.9433801486342004, - "sample_size": 18, - "files_processed": 1202, - "files_total": 1240, - "files_iterated": 1194 - }, - { - "name": "tsv_wasm", - "group": "format/svelte", - "mean_ns": 348305291.1666667, - "p50_ns": 348407825, - "p75_ns": 349005457.5, - "p90_ns": 351966927.4, - "p95_ns": 353359477.1, - "p99_ns": 355469057.02, - "min_ns": 347712508, - "max_ns": 355996452, - "std_dev_ns": 403574.7899962474, - "cv": 0.0011586811921359351, - "ops_per_second": 2.8710445272032703, - "sample_size": 12, - "files_processed": 1202, - "files_total": 1240, - "files_iterated": 1194 - }, - { - "name": "oxfmt", - "group": "format/svelte", - "mean_ns": 3363123440.8, - "p50_ns": 3355465187, - "p75_ns": 3380464215, - "p90_ns": 3387858512.4, - "p95_ns": 3390323278.2, - "p99_ns": 3392295090.84, - "min_ns": 3336319057, - "max_ns": 3392788044, - "std_dev_ns": 22995300.045456335, - "cv": 0.006837483205786329, - "ops_per_second": 0.2973426392467253, - "sample_size": 5, - "files_processed": 1201, - "files_total": 1240, - "files_iterated": 1194 - }, - { - "name": "biome-wasm", - "group": "format/svelte", - "mean_ns": 651628305.75, - "p50_ns": 651348032.5, - "p75_ns": 652776746.25, - "p90_ns": 655602311.7, - "p95_ns": 657068072.85, - "p99_ns": 658240681.77, - "min_ns": 646287594, - "max_ns": 658533834, - "std_dev_ns": 3695077.8107031505, - "cv": 0.005670529929558927, - "ops_per_second": 1.5346171907757093, - "sample_size": 8, - "files_processed": 1238, - "files_total": 1240, - "files_iterated": 1194 - }, - { - "name": "acorn-typescript", - "group": "parse/typescript", - "mean_ns": 2213405912.6, - "p50_ns": 2212095774, - "p75_ns": 2213405114, - "p90_ns": 2218802607.8, - "p95_ns": 2220601772.4, - "p99_ns": 2222041104.08, - "min_ns": 2209487030, - "max_ns": 2222400937, - "std_dev_ns": 5295312.414249306, - "cv": 0.0023923819775240017, - "ops_per_second": 0.4517924138123132, - "sample_size": 5, - "files_processed": 3693, - "files_total": 4025, - "files_iterated": 3603 - }, - { - "name": "tsv-json", - "group": "parse/typescript", - "mean_ns": 2049624429.8, - "p50_ns": 2046277896, - "p75_ns": 2052475955, - "p90_ns": 2062325034.8, - "p95_ns": 2065608061.4, - "p99_ns": 2068234482.68, - "min_ns": 2036381168, - "max_ns": 2068891088, - "std_dev_ns": 12211486.98364508, - "cv": 0.005957914438420635, - "ops_per_second": 0.4878942627052796, - "sample_size": 5, - "files_processed": 3743, - "files_total": 4025, - "files_iterated": 3603 - }, - { - "name": "tsv_wasm-json", - "group": "parse/typescript", - "mean_ns": 2223665077.4, - "p50_ns": 2223809451, - "p75_ns": 2224511693, - "p90_ns": 2224643608.4, - "p95_ns": 2224687580.2, - "p99_ns": 2224722757.64, - "min_ns": 2222334633, - "max_ns": 2224731552, - "std_dev_ns": 1021470.6667904371, - "cv": 0.0004593635422762416, - "ops_per_second": 0.44970801141026184, - "sample_size": 5, - "files_processed": 3743, - "files_total": 4025, - "files_iterated": 3603 - }, - { - "name": "tsv-internal", - "group": "parse/typescript", - "mean_ns": 236250428.1875, - "p50_ns": 235985787, - "p75_ns": 239292380, - "p90_ns": 262673433, - "p95_ns": 263116119, - "p99_ns": 263948169.4, - "min_ns": 234967322, - "max_ns": 264156182, - "std_dev_ns": 1336552.468610353, - "cv": 0.005657354692917632, - "ops_per_second": 4.232796561140413, - "sample_size": 16, - "files_processed": 3743, - "files_total": 4025, - "files_iterated": 3603 - }, - { - "name": "tsv_wasm-internal", - "group": "parse/typescript", - "mean_ns": 292595892.125, - "p50_ns": 292647736.5, - "p75_ns": 292773388.75, - "p90_ns": 293666035.6, - "p95_ns": 294709175.9, - "p99_ns": 294963799.18, - "min_ns": 292266142, - "max_ns": 295027455, - "std_dev_ns": 262236.36256371846, - "cv": 0.0008962407525929597, - "ops_per_second": 3.4176829781765687, - "sample_size": 16, - "files_processed": 3743, - "files_total": 4025, - "files_iterated": 3603 - }, - { - "name": "oxc-parser", - "group": "parse/typescript", - "mean_ns": 864942821.5, - "p50_ns": 863969686.5, - "p75_ns": 878826551, - "p90_ns": 884913960, - "p95_ns": 887896262.5, - "p99_ns": 890282104.5, - "min_ns": 845083325, - "max_ns": 890878565, - "std_dev_ns": 20069394.017162047, - "cv": 0.023203145362091483, - "ops_per_second": 1.1561457880716106, - "sample_size": 6, - "files_processed": 3833, - "files_total": 4025, - "files_iterated": 3603 - }, - { - "name": "oxc-parser-wasm", - "group": "parse/typescript", - "mean_ns": 976785685.3333334, - "p50_ns": 975594108.5, - "p75_ns": 979113434.75, - "p90_ns": 982226200, - "p95_ns": 983362576, - "p99_ns": 984271676.8, - "min_ns": 971966753, - "max_ns": 984498952, - "std_dev_ns": 4714293.024074751, - "cv": 0.004826333037902754, - "ops_per_second": 1.023766026688592, - "sample_size": 6, - "files_processed": 4025, - "files_total": 4025, - "files_iterated": 3603 - }, - { - "name": "prettier", - "group": "format/typescript", - "mean_ns": 8773944013.666666, - "p50_ns": 8784008051, - "p75_ns": 8807150369, - "p90_ns": 8902301987.4, - "p95_ns": 8961594658.2, - "p99_ns": 9009028794.84, - "min_ns": 8712430383, - "max_ns": 9020887329, - "std_dev_ns": 36684367.115987, - "cv": 0.004181057806939032, - "ops_per_second": 0.1139738296075696, - "sample_size": 6, - "files_processed": 3855, - "files_total": 4025, - "files_iterated": 3701 - }, - { - "name": "tsv", - "group": "format/typescript", - "mean_ns": 854559033.5, - "p50_ns": 856988296.5, - "p75_ns": 860171191.5, - "p90_ns": 861313209, - "p95_ns": 861398115.5, - "p99_ns": 861466040.7, - "min_ns": 838541329, - "max_ns": 861483022, - "std_dev_ns": 8547810.99796243, - "cv": 0.01000259860685497, - "ops_per_second": 1.1701941712608437, - "sample_size": 6, - "files_processed": 3743, - "files_total": 4025, - "files_iterated": 3701 - }, - { - "name": "tsv_wasm", - "group": "format/typescript", - "mean_ns": 1145280037.8, - "p50_ns": 1144938011, - "p75_ns": 1145815746, - "p90_ns": 1146670300.2, - "p95_ns": 1146955151.6, - "p99_ns": 1147183032.72, - "min_ns": 1144152898, - "max_ns": 1147240003, - "std_dev_ns": 1281560.5264546424, - "cv": 0.0011189931581418527, - "ops_per_second": 0.8731488954622204, - "sample_size": 5, - "files_processed": 3743, - "files_total": 4025, - "files_iterated": 3701 - }, - { - "name": "oxfmt", - "group": "format/typescript", - "mean_ns": 802078434.8571428, - "p50_ns": 804423326, - "p75_ns": 806569662, - "p90_ns": 809150059.2, - "p95_ns": 809881109.1, - "p99_ns": 810465949.02, - "min_ns": 792012009, - "max_ns": 810612159, - "std_dev_ns": 7054022.950975906, - "cv": 0.008794679727590843, - "ops_per_second": 1.246760860960074, - "sample_size": 7, - "files_processed": 3836, - "files_total": 4025, - "files_iterated": 3701 - }, - { - "name": "biome-wasm", - "group": "format/typescript", - "mean_ns": 3651240436.25, - "p50_ns": 3651115766, - "p75_ns": 3652416674, - "p90_ns": 3654275795, - "p95_ns": 3654895502, - "p99_ns": 3655391267.6, - "min_ns": 3650446557, - "max_ns": 3655515209, - "std_dev_ns": 835808.0747653235, - "cv": 0.00022891071934548872, - "ops_per_second": 0.2738795260021409, - "sample_size": 4, - "files_processed": 4025, - "files_total": 4025, - "files_iterated": 3701 - }, - { - "name": "svelte/compiler", - "group": "parse/css", - "mean_ns": 19403785.678294573, - "p50_ns": 19376518.5, - "p75_ns": 19515459.5, - "p90_ns": 19626101.3, - "p95_ns": 19749809.1, - "p99_ns": 19835973.34, - "min_ns": 19083462, - "max_ns": 19889772, - "std_dev_ns": 170231.79292084067, - "cv": 0.008773122716525623, - "ops_per_second": 51.53633505232013, - "sample_size": 258, - "files_processed": 163, - "files_total": 193, - "files_iterated": 159 - }, - { - "name": "tsv-json", - "group": "parse/css", - "mean_ns": 176879819.27586207, - "p50_ns": 176417640, - "p75_ns": 178453737, - "p90_ns": 179335842.4, - "p95_ns": 179414368, - "p99_ns": 179869721.35999998, - "min_ns": 173847338, - "max_ns": 180029857, - "std_dev_ns": 1802916.9896241936, - "cv": 0.010192892535763852, - "ops_per_second": 5.653556206094932, - "sample_size": 29, - "files_processed": 165, - "files_total": 193, - "files_iterated": 159 - }, - { - "name": "tsv_wasm-json", - "group": "parse/css", - "mean_ns": 223033824.05555555, - "p50_ns": 222987561, - "p75_ns": 224042748.5, - "p90_ns": 226002163.2, - "p95_ns": 226359314, - "p99_ns": 226689078.04, - "min_ns": 222564338, - "max_ns": 226779975, - "std_dev_ns": 476931.02029914665, - "cv": 0.002138379783060832, - "ops_per_second": 4.483624868266213, - "sample_size": 18, - "files_processed": 165, - "files_total": 193, - "files_iterated": 159 - }, - { - "name": "tsv-internal", - "group": "parse/css", - "mean_ns": 23123759.831578948, - "p50_ns": 23081315.5, - "p75_ns": 23581269.25, - "p90_ns": 25285569.600000005, - "p95_ns": 26399632.149999995, - "p99_ns": 34455670.899999976, - "min_ns": 22528124, - "max_ns": 43861736, - "std_dev_ns": 454153.9601688399, - "cv": 0.019640143448844544, - "ops_per_second": 43.24556245539061, - "sample_size": 190, - "files_processed": 165, - "files_total": 193, - "files_iterated": 159 - }, - { - "name": "tsv_wasm-internal", - "group": "parse/css", - "mean_ns": 32072885.51879699, - "p50_ns": 32066954.5, - "p75_ns": 32148053.5, - "p90_ns": 32335215.5, - "p95_ns": 32349665.75, - "p99_ns": 32378088.75, - "min_ns": 31983936, - "max_ns": 32565198, - "std_dev_ns": 53586.24561517934, - "cv": 0.0016707647206789045, - "ops_per_second": 31.178984485631293, - "sample_size": 133, - "files_processed": 165, - "files_total": 193, - "files_iterated": 159 - }, - { - "name": "prettier", - "group": "format/css", - "mean_ns": 731055145.8571428, - "p50_ns": 726866934, - "p75_ns": 737411474, - "p90_ns": 739494874, - "p95_ns": 740913023.5, - "p99_ns": 742047543.1, - "min_ns": 723869573, - "max_ns": 742331173, - "std_dev_ns": 7724050.9095097985, - "cv": 0.010565620053810789, - "ops_per_second": 1.3678858642428766, - "sample_size": 7, - "files_processed": 192, - "files_total": 193, - "files_iterated": 165 - }, - { - "name": "tsv", - "group": "format/css", - "mean_ns": 42276700.15044248, - "p50_ns": 42363994, - "p75_ns": 42798711, - "p90_ns": 43541791.3, - "p95_ns": 44634919.449999996, - "p99_ns": 50024498.75, - "min_ns": 40686670, - "max_ns": 51286794, - "std_dev_ns": 850647.2521532943, - "cv": 0.020120947215043962, - "ops_per_second": 23.65369095604624, - "sample_size": 113, - "files_processed": 165, - "files_total": 193, - "files_iterated": 165 - }, - { - "name": "tsv_wasm", - "group": "format/css", - "mean_ns": 56842907.02898551, - "p50_ns": 56836126.5, - "p75_ns": 57051209.25, - "p90_ns": 57277215.5, - "p95_ns": 57411214.85, - "p99_ns": 57614455.839999996, - "min_ns": 56701796, - "max_ns": 57937459, - "std_dev_ns": 94515.15490540798, - "cv": 0.0016627431608522859, - "ops_per_second": 17.59234445011894, - "sample_size": 69, - "files_processed": 165, - "files_total": 193, - "files_iterated": 165 - }, - { - "name": "oxfmt", - "group": "format/css", - "mean_ns": 759715951, - "p50_ns": 758504294, - "p75_ns": 764356670.5, - "p90_ns": 792120220.8000001, - "p95_ns": 810371960.4, - "p99_ns": 824973352.0799999, - "min_ns": 755827467, - "max_ns": 828623700, - "std_dev_ns": 4300372.980217599, - "cv": 0.005660501105126328, - "ops_per_second": 1.3162814321375227, - "sample_size": 6, - "files_processed": 192, - "files_total": 193, - "files_iterated": 165 - }, - { - "name": "biome-wasm", - "group": "format/css", - "mean_ns": 207351523.875, - "p50_ns": 207159397, - "p75_ns": 208264123, - "p90_ns": 208754043.4, - "p95_ns": 209395575, - "p99_ns": 209795343.92, - "min_ns": 205889964, - "max_ns": 209888054, - "std_dev_ns": 897025.2129294197, - "cv": 0.004326108610950857, - "ops_per_second": 4.822727999832984, - "sample_size": 24, - "files_processed": 193, - "files_total": 193, - "files_iterated": 165 - } - ], - "suppressed_noise": { - "oxfmt::textToDoc": 1, - "panicked at crates/biome_rowan": 1 - } -} diff --git a/benches/deno/results/report.md b/benches/deno/results/report.md deleted file mode 100644 index 32657ac36..000000000 --- a/benches/deno/results/report.md +++ /dev/null @@ -1,193 +0,0 @@ -# tsv benchmark results - -**Date:** 2026-06-12T09:22:00.588Z (9b5c4e98) - -**Corpus:** 1240 Svelte (1.6 MB), 4025 TypeScript (13.1 MB), 193 CSS (0.7 MB) — 5458 files, 15.4 MB total - -**Versions:** svelte@5.56.1, acorn@8.16.0, acorn-typescript@1.0.10, prettier@3.8.3, prettier-plugin-svelte@3.5.2, oxc-parser@0.134.0, oxfmt@0.53.0, @biomejs/wasm-bundler@2.4.16 - -**Methodology:** Single-threaded — every implementation formats/parses one file at a time, measured sequentially with no cross-file parallelism. The numbers are per-file, single-core latency/throughput, not the multi-core batch throughput a CLI gets formatting many files at once. - -## parse/svelte - -| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs svelte/compiler (speedup) | -| ----------------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------------------------- | -| svelte/compiler | 2.83 | 15 | 351.79 | 355.71 | 358.76 | 359.83 | 361.46 | 349.59 | 361.86 | baseline | -| tsv-json | 2.06 | 9 | 485.09 | 487.64 | 491.66 | — | — | 481.78 | 496.86 | 0.73x | -| tsv_wasm-json | 1.94 | 7 | 516.07 | 517.32 | 519.26 | — | — | 515.56 | 522.28 | 0.69x | -| tsv-internal | 27.00 | 119 | 36.82 | 38.03 | 39.75 | 49.37 | 70.56 | 35.49 | 73.56 | 9.55x | -| tsv_wasm-internal | 22.10 | 102 | 45.24 | 45.35 | 45.45 | 45.62 | 45.69 | 45.09 | 45.72 | 7.81x | - -**Files (intersection):** 1192 - -**Throughput:** svelte/compiler 4.5 MB/s, tsv-json 3.3 MB/s, tsv_wasm-json 3.1 MB/s, tsv-internal 43.4 MB/s, tsv_wasm-internal 35.5 MB/s - -**Coverage:** svelte/compiler 1193/1240 (96%), tsv-json 1202/1240 (96%), tsv_wasm-json 1202/1240 (96%), tsv-internal 1202/1240 (96%), tsv_wasm-internal 1202/1240 (96%) - -**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 13.1x tsv-internal, tsv_wasm-json 11.4x tsv_wasm-internal - -## format/svelte - -| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs prettier (speedup) | -| ---------- | ------- | -- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | --------------------- | -| prettier | 0.32 | 5 | 3153.33 | 3161.18 | 3191.38 | — | — | 3116.68 | 3211.52 | baseline | -| tsv | 3.94 | 18 | 250.90 | 261.50 | 270.31 | 291.69 | 299.63 | 247.14 | 301.61 | 12.4x | -| tsv_wasm | 2.87 | 12 | 348.41 | 349.01 | 351.97 | 353.36 | 355.47 | 347.71 | 356.00 | 9.06x | -| oxfmt | 0.30 | 5 | 3355.47 | 3380.46 | 3387.86 | — | — | 3336.32 | 3392.79 | 0.94x | -| biome-wasm | 1.53 | 8 | 651.35 | 652.78 | 655.60 | — | — | 646.29 | 658.53 | 4.84x | - -**Files (intersection):** 1194 - -**Throughput:** prettier 0.5 MB/s, tsv 6.1 MB/s, tsv_wasm 4.5 MB/s, oxfmt 0.5 MB/s, biome-wasm 2.4 MB/s - -**Coverage:** prettier 1201/1240 (96%), tsv 1202/1240 (96%), tsv_wasm 1202/1240 (96%), oxfmt 1201/1240 (96%), biome-wasm 1238/1240 (99%) - -## parse/typescript - -| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs acorn-typescript (speedup) | -| ----------------- | ------- | -- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ----------------------------- | -| acorn-typescript | 0.45 | 5 | 2212.10 | 2213.41 | 2218.80 | — | — | 2209.49 | 2222.40 | baseline | -| tsv-json | 0.49 | 5 | 2046.28 | 2052.48 | 2062.33 | — | — | 2036.38 | 2068.89 | 1.08x | -| tsv_wasm-json | 0.45 | 5 | 2223.81 | 2224.51 | 2224.64 | — | — | 2222.33 | 2224.73 | 1.00x | -| tsv-internal | 4.23 | 16 | 235.99 | 239.29 | 262.67 | 263.12 | 263.95 | 234.97 | 264.16 | 9.37x | -| tsv_wasm-internal | 3.42 | 16 | 292.65 | 292.77 | 293.67 | 294.71 | 294.96 | 292.27 | 295.03 | 7.56x | -| oxc-parser | 1.16 | 6 | 863.97 | 878.83 | 884.91 | — | — | 845.08 | 890.88 | 2.56x | -| oxc-parser-wasm | 1.02 | 6 | 975.59 | 979.11 | 982.23 | — | — | 971.97 | 984.50 | 2.27x | - -**Files (intersection):** 3603 - -**Throughput:** acorn-typescript 5.8 MB/s, tsv-json 6.3 MB/s, tsv_wasm-json 5.8 MB/s, tsv-internal 54.8 MB/s, tsv_wasm-internal 44.2 MB/s, oxc-parser 15.0 MB/s, oxc-parser-wasm 13.3 MB/s - -**Coverage:** acorn-typescript 3693/4025 (91%), tsv-json 3743/4025 (92%), tsv_wasm-json 3743/4025 (92%), tsv-internal 3743/4025 (92%), tsv_wasm-internal 3743/4025 (92%), oxc-parser 3833/4025 (95%), oxc-parser-wasm 4025/4025 (100%) - -**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 8.7x tsv-internal, tsv_wasm-json 7.6x tsv_wasm-internal - -## format/typescript - -| Task Name | ops/sec | n | p50 (s) | p75 (s) | p90 (s) | p95 (s) | p99 (s) | min (s) | max (s) | vs prettier (speedup) | -| ---------- | ------- | - | ------- | ------- | ------- | ------- | ------- | ------- | ------- | --------------------- | -| prettier | 0.11 | 6 | 8.78 | 8.81 | 8.90 | — | — | 8.71 | 9.02 | baseline | -| tsv | 1.17 | 6 | 0.86 | 0.86 | 0.86 | — | — | 0.84 | 0.86 | 10.3x | -| tsv_wasm | 0.87 | 5 | 1.14 | 1.15 | 1.15 | — | — | 1.14 | 1.15 | 7.66x | -| oxfmt | 1.25 | 7 | 0.80 | 0.81 | 0.81 | — | — | 0.79 | 0.81 | 10.9x | -| biome-wasm | 0.27 | 4 | 3.65 | 3.65 | 3.65 | — | — | 3.65 | 3.66 | 2.40x | - -**Files (intersection):** 3701 - -**Throughput:** prettier 1.5 MB/s, tsv 15.2 MB/s, tsv_wasm 11.3 MB/s, oxfmt 16.2 MB/s, biome-wasm 3.6 MB/s - -**Coverage:** prettier 3855/4025 (95%), tsv 3743/4025 (92%), tsv_wasm 3743/4025 (92%), oxfmt 3836/4025 (95%), biome-wasm 4025/4025 (100%) - -## parse/css - -| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs svelte/compiler (speedup) | -| ----------------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------------------------- | -| svelte/compiler | 51.54 | 258 | 19.38 | 19.52 | 19.63 | 19.75 | 19.84 | 19.08 | 19.89 | baseline | -| tsv-json | 5.65 | 29 | 176.42 | 178.45 | 179.34 | 179.41 | 179.87 | 173.85 | 180.03 | 0.11x | -| tsv_wasm-json | 4.48 | 18 | 222.99 | 224.04 | 226.00 | 226.36 | 226.69 | 222.56 | 226.78 | 0.09x | -| tsv-internal | 43.25 | 190 | 23.08 | 23.58 | 25.29 | 26.40 | 34.46 | 22.53 | 43.86 | 0.84x | -| tsv_wasm-internal | 31.18 | 133 | 32.07 | 32.15 | 32.34 | 32.35 | 32.38 | 31.98 | 32.57 | 0.60x | - -**Files (intersection):** 159 - -**Throughput:** svelte/compiler 35.2 MB/s, tsv-json 3.9 MB/s, tsv_wasm-json 3.1 MB/s, tsv-internal 29.5 MB/s, tsv_wasm-internal 21.3 MB/s - -**Coverage:** svelte/compiler 163/193 (84%), tsv-json 165/193 (85%), tsv_wasm-json 165/193 (85%), tsv-internal 165/193 (85%), tsv_wasm-internal 165/193 (85%) - -**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 7.6x tsv-internal, tsv_wasm-json 7.0x tsv_wasm-internal - -## format/css - -| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs prettier (speedup) | -| ---------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | --------------------- | -| prettier | 1.37 | 7 | 726.87 | 737.41 | 739.49 | — | — | 723.87 | 742.33 | baseline | -| tsv | 23.65 | 113 | 42.36 | 42.80 | 43.54 | 44.63 | 50.02 | 40.69 | 51.29 | 17.3x | -| tsv_wasm | 17.59 | 69 | 56.84 | 57.05 | 57.28 | 57.41 | 57.61 | 56.70 | 57.94 | 12.9x | -| oxfmt | 1.32 | 6 | 758.50 | 764.36 | 792.12 | — | — | 755.83 | 828.62 | 0.96x | -| biome-wasm | 4.82 | 24 | 207.16 | 208.26 | 208.75 | 209.40 | 209.80 | 205.89 | 209.89 | 3.53x | - -**Files (intersection):** 165 - -**Throughput:** prettier 0.9 MB/s, tsv 16.4 MB/s, tsv_wasm 12.2 MB/s, oxfmt 0.9 MB/s, biome-wasm 3.3 MB/s - -**Coverage:** prettier 192/193 (99%), tsv 165/193 (85%), tsv_wasm 165/193 (85%), oxfmt 192/193 (99%), biome-wasm 193/193 (100%) - -_Note: every `Nx` is speedup form — values > 1 mean self is faster. File counts come from the per-group `Files (intersection):` / `Coverage:` lines and the Comparisons table row labels._ - -## Binary Sizes - -| Binary | Size | Gzipped | vs tsv | vs tsv (gz) | -| ------------------------- | ------: | -------: | -----: | ----------: | -| tsv_format_wasm | 2.2 MB | 715.4 KB | 0.8x | 0.8x | -| tsv_parse_wasm | 1.7 MB | 526.6 KB | 0.6x | 0.6x | -| tsv_wasm | 2.9 MB | 911.3 KB | — | — | -| biome (wasm) | 34.4 MB | 8.2 MB | 11.9x | 9.0x | -| oxc-parser (wasm) | 1.9 MB | 518.7 KB | 0.6x | 0.6x | -| tsv (native) | 3.9 MB | 1.6 MB | — | — | -| oxc-parser+oxfmt (native) | 10.7 MB | 4.3 MB | 2.8x | 2.6x | -| oxc-parser (native) | 2.7 MB | 1.0 MB | 0.7x | 0.6x | -| oxfmt (native) | 8.0 MB | 3.2 MB | 2.1x | 2.0x | - -_Gzipped ≈ npm-tarball wire size (`gzip -c`, system default level). `vs tsv (gz)` compares gzipped bytes; `vs tsv` compares raw on-disk bytes._ - -## Comparisons to tsv (speedup) - -| Benchmark | Comparisons | -| ------------------------- | -------------------------------------- | -| format svelte (1194f) | **12.4x** prettier, **13.3x** oxfmt | -| format typescript (3701f) | **10.3x** prettier, **0.94x** oxfmt | -| format css (165f) | **17.3x** prettier, **18.0x** oxfmt | -| parse svelte (1192f) | **0.73x** svelte | -| parse typescript (3603f) | **1.08x** svelte, **0.42x** oxc-parser | -| parse css (159f) | **0.11x** svelte | - -## Comparisons to tsv_wasm (speedup) - -| Benchmark | Comparisons | -| ------------------------- | ------------------------------------------- | -| format svelte (1194f) | **9.06x** prettier, **1.87x** biome-wasm | -| format typescript (3701f) | **7.66x** prettier, **3.19x** biome-wasm | -| format css (165f) | **12.9x** prettier, **3.65x** biome-wasm | -| parse svelte (1192f) | **0.69x** svelte | -| parse typescript (3603f) | **1.00x** svelte, **0.44x** oxc-parser-wasm | -| parse css (159f) | **0.09x** svelte | - -_`Nx` is speedup — self is N× faster than the named opponent. `(Mf)` is the self impl's iterated count (per-group intersection in default mode; per-impl success set in `BENCH_MODE=union`). Parse canonical: svelte/compiler for .svelte/.css, acorn-typescript for .ts. oxc-parser (native and wasm) serializes the AST to JSON in Rust and deserializes it in JS — the same eager materialization as tsv-json/tsv_wasm-json, so these parse rows are apples-to-apples. tsv-internal/tsv_wasm-internal are parse-only (no JS materialization) and have no oxc counterpart — oxc exposes no comparably cheap mode (its JS API always serializes; experimentalLazy is setup-dominated). Format groups include parse time — each formatter parses internally._ - -## Skipped Files - -1390 unique file+error combinations — Svelte 165, TypeScript 1165, CSS 60. - -**Per-benchmark skip counts:** - -- parse/typescript: acorn-typescript: 332 -- parse/typescript: tsv-json: 282 -- parse/typescript: tsv_wasm-json: 282 -- parse/typescript: tsv-internal: 282 -- parse/typescript: tsv_wasm-internal: 282 -- format/typescript: tsv: 282 -- format/typescript: tsv_wasm: 282 -- parse/typescript: oxc-parser: 192 -- format/typescript: oxfmt: 189 -- format/typescript: prettier: 170 -- parse/svelte: svelte/compiler: 47 -- format/svelte: prettier: 39 -- format/svelte: oxfmt: 39 -- parse/svelte: tsv-json: 38 -- parse/svelte: tsv_wasm-json: 38 -- parse/svelte: tsv-internal: 38 -- parse/svelte: tsv_wasm-internal: 38 -- format/svelte: tsv: 38 -- format/svelte: tsv_wasm: 38 -- parse/css: svelte/compiler: 30 -- parse/css: tsv-json: 28 -- parse/css: tsv_wasm-json: 28 -- parse/css: tsv-internal: 28 -- parse/css: tsv_wasm-internal: 28 -- format/css: tsv: 28 -- format/css: tsv_wasm: 28 -- format/svelte: biome-wasm: 2 -- format/css: prettier: 1 -- format/css: oxfmt: 1 - -_Per-file detail omitted. Re-run with `--verbose` to include error messages and failure sets per file._ diff --git a/benches/deno/CLAUDE.md b/benches/js/CLAUDE.md similarity index 74% rename from benches/deno/CLAUDE.md rename to benches/js/CLAUDE.md index 76732fd43..17bfcacbc 100644 --- a/benches/deno/CLAUDE.md +++ b/benches/js/CLAUDE.md @@ -4,6 +4,71 @@ Uses [@fuzdev/fuz_util](https://github.com/fuzdev/fuz_util) benchmarking library for statistical analysis. +> **Directory note:** this directory (renamed from the legacy `benches/deno/` — +> the harness is JS-runtime-agnostic, not Deno's) is the **runtime-neutral** +> harness home: the same code runs under Deno, Node, and Bun (see +> [Cross-Runtime](#cross-runtime-deno--node--bun)). The `corpus_compare_*` and +> `diagnostics/` entries stay Deno-idiomatic; `smoke` is portable across all +> three (`smoke` / `smoke:node` / `smoke:bun`). + +## Cross-Runtime (Deno + Node + Bun) + +The bench runs under **Deno, Node, and Bun** from one shared codebase. +The motivation: a single-runtime bench can silently fold a runtime-specific effect +into an engine number — the concrete case being the Deno-FFI fast-call memory +sensitivity that mismeasured the native path (see §Known Issues + the lore +`TODO_NODE_BENCHMARKS.md`). A per-runtime delta on the same row is the detector. + +**Design (well-factored, not forked):** + +- **Runtime-labeled sibling reports.** Each runtime writes its own + `results/report..{json,md}` (+ a timestamped `…_..*` + pair), same schema, never merged. Every row carries a `runtime` field; the JSON + has a top-level `runtime` and `version: 5`. `deno task bench:compose` (run at + the end of `deno task bench`) then folds the siblings into a compact combined + `results/report.{json,md}` — the cross-runtime view tsv.fuz.dev consumes + (`compose_reports.ts`; a per-runtime delta on a row is the headline). +- **One bench body, runtime-detected.** `bench.ts` runs under both — it detects + the runtime (`lib/runtime.ts` `current_runtime()`) and selects the + runtime-specific artifacts. No forked entry; `bench:node:run` is literally + `node benches/js/bench.ts`. +- **Portable shared modules.** The shared/entry modules use `node:` builtins + (Deno supports them) + `@fuzdev/fuz_util` helpers (`fs_search`, `fs_exists`, + `spawn_out`, `to_file_path`) — **no `Deno.*`, no `@std/*`**. The only genuinely + runtime-specific files are the native loader (`ffi.ts` `Deno.dlopen` vs + `napi.ts` `process.dlopen`) and the WASM target the loader picks. Deno-only + tools (`corpus_compare_*`, `diagnostics/*`, the `deno test` suite) stay + Deno-idiomatic (`Deno.*` / `@std/fs`). +- **The native row differs by runtime, fairly.** Deno → FFI (`tsv_ffi`, via + `Deno.dlopen`); Node → N-API (`tsv_napi`, via `process.dlopen`). Same engine, + same per-thread arena reuse, different binding boundary. +- **The WASM row uses each runtime's own wasm-pack target bundle** (same + `tsv_wasm_bg.wasm`, different JS glue) — Deno the `deno` target, Node the + `nodejs` target — both with the full export set incl. `parse_internal_*`. The + shipped web bundle is deliberately not used (it curates `parse_internal_*` out). + +**Dependencies: `package.json` is the source of truth.** Both runtimes consume one +`node_modules` (`benches/js/package.json`). Deno reads it via +`"nodeModulesDir": "manual"` in `deno.json`; Node reads it directly. Only jsr-only +specifiers (`@std/fs`, used by the Deno-only diagnostics) stay in the deno.json +import map. `@types/node` is a types-only devDependency so the `node:` builtins +type-check under `deno check`/`deno test`. + +**Install with `deno task bench:install`** (runs `benches/js/install_deps.ts`). +It is the canonical installer: a plain `npm install` works but **prunes the +`@oxc-parser/binding-wasm32-wasi` binding** (the oxc-parser-wasm row) — that +binding is pure-wasm but its metadata declares `cpu: wasm32`, so npm skips it on +any non-wasm host and reconciles away a `--no-save` add. `install_deps.ts` runs +`npm install` then force-fetches it at the `oxc-parser` version (oxc ships all +bindings in lockstep). After a bump or a stray `npm install`, re-run +`bench:install`. + +**oxc-parser-wasm runs under all three.** Its binding ships two entries — a +fetch-based browser entry (`parser.wasi-browser.js`) that Deno needs, and the +default `node:wasi` entry that Node/Bun need — so `oxc_wasm.ts` picks the right +one per runtime (`current_runtime()`). (Node also has oxc-parser **native**, the +more relevant Node number, regardless.) + ## Corpus Comparison Compare formatting output against Prettier on arbitrary codebases: @@ -230,66 +295,78 @@ Every pattern in `patterns.ts` includes: ## Benchmark Commands -Results are saved to `benches/deno/results/` as timestamped files plus -`report.json` and `report.md` (committed to git). To publish benchmarks -to tsv.fuz.dev, run `npm run update-benchmarks` in ~/dev/tsv.fuz.dev. - -The committed `report.json` (baseline `version: 4`) carries, beyond timing -stats: per-language `corpus` totals, `versions`, and `binary_sizes` (each with -`gzip_bytes`). Each `entries[]` row adds `files_processed`/`files_total` -(per-impl preflight coverage — the `Coverage:` line) and `files_iterated` (the -timed set — the `Files (intersection):` count). A top-level `suppressed_noise` -map records silenced third-party stderr crashes as `{pattern: count}`. -`report.md` renders coverage/iterated as prose; the per-entry numbers and -`suppressed_noise` are JSON-only. +Each runtime saves to `benches/js/results/` as timestamped files plus a +committed `report..{json,md}` pair (`report.deno.*` / `report.node.*`). +To publish benchmarks to tsv.fuz.dev, run `npm run update-benchmarks` in +~/dev/tsv.fuz.dev (it reads the per-runtime files — see the cross-repo note in +the lore when renaming from the old single `report.json`). + +The committed `report..json` (baseline `version: 5`) carries, beyond +timing stats: a top-level `runtime`, per-language `corpus` totals, `versions`, +and `binary_sizes` (each with `gzip_bytes`). Each `entries[]` row adds a +`runtime` field, `files_processed`/`files_total` (per-impl preflight coverage — +the `Coverage:` line) and `files_iterated` (the timed set — the `Files +(intersection):` count). A top-level `suppressed_noise` map records silenced +third-party stderr crashes as `{pattern: count}`. `report..md` renders +coverage/iterated as prose; the per-entry numbers and `suppressed_noise` are +JSON-only. ```bash -# Run benchmarks (builds the bench artifact set — `build:bench` — automatically) -deno task bench +deno task bench:install # one-time: install harness npm deps (see Cross-Runtime above) + +# Run benchmarks (builds the runtime's bench artifacts automatically) +deno task bench # ALL three runtimes + compose → report.{deno,node,bun}.* + combined report.{json,md} +deno task bench:deno # Deno only +deno task bench:node # Node only +deno task bench:bun # Bun only (reuses the Node artifacts — N-API + nodejs-target WASM) +deno task bench:compose # Fold existing report.{deno,node,bun}.json → combined report.{json,md} # Run without rebuilding (if already built) — guarded against stale artifacts, # see "Artifact Freshness Guard" below -deno task bench:run - -# Output formats -deno task bench:run -- --json # Output as JSON (for CI/tooling) -deno task bench:run -- --markdown # Output as Markdown tables -deno task bench:run -- --verbose # Include per-file skip detail (paths + errors) -deno task bench:run -- --save-report # Force-overwrite the committed results/report.{json,md} - # on a limited/filtered run (full runs overwrite it anyway; - # the timestamped results/_.{json,md} pair is always written) +deno task bench:deno:run +deno task bench:node:run +deno task bench:bun:run + +# Output formats / flags (shown for :deno:run; same for :node:run) +deno task bench:deno:run -- --json # Output as JSON (for CI/tooling) +deno task bench:deno:run -- --markdown # Output as Markdown tables +deno task bench:deno:run -- --verbose # Include per-file skip detail (paths + errors) +deno task bench:deno:run -- --save-report # Force-overwrite the committed report..{json,md} + # on a limited/filtered run (full runs overwrite it anyway; + # the timestamped results/_..* pair is always written) # Baseline regression detection -deno task bench:run -- --save-baseline # Save current results as baseline -deno task bench:run -- --compare-baseline # Compare against saved baseline +deno task bench:deno:run -- --save-baseline # Save current results as baseline +deno task bench:deno:run -- --compare-baseline # Compare against saved baseline # Wipe local-only bench state (gitignored): baseline.json + timestamped -# results pairs. Preserves the committed `results/report.{json,md}` because +# results pairs. Preserves the committed `report..{json,md}` because # the glob is anchored on a leading digit (timestamped files start with a year). deno task bench:clean -# Environment variables -BENCH_LIMIT=5 deno task bench:run # Limit files per language (default: all) -BENCH_FILTER=zzz deno task bench:run # Filter by path pattern (default: none) -BENCH_DURATION=10000 deno task bench:run # Duration per benchmark in ms (default: 5000) -BENCH_WARMUP=10 deno task bench:run # Set warmup iterations (default: 3) -BENCH_MODE=union deno task bench:run # Per-impl iteration (default: intersection) -BENCH_STALE_OK=1 deno task bench:run # Override the freshness guard (see below; default: off) -BENCH_FORCED_ASYNC=1 deno task bench:run # Add the tsv-forced-async control row (diagnostic; default: off) +# Environment variables (apply to any runtime's :run) +BENCH_LIMIT=5 deno task bench:deno:run +BENCH_FILTER=zzz deno task bench:deno:run +BENCH_DURATION=10000 deno task bench:deno:run +BENCH_WARMUP=10 deno task bench:deno:run +BENCH_MODE=union deno task bench:deno:run +BENCH_STALE_OK=1 deno task bench:deno:run +BENCH_FORCED_ASYNC=1 deno task bench:deno:run ``` ## Artifact Freshness Guard -The rebuild-skipping tasks (`bench:run`, `corpus:compare:format:run`, and -`smoke`) skip the rebuild so you can iterate on the harness without paying the -wasm-pack cost — at the risk of silently measuring a binary older than current -source (a CSS run once reported `146/183` against a stale `.so` that should have -been `155/183`). `lib/check_artifact_freshness.ts` guards against this: before a -run touches the executed artifacts (the profile-aware FFI library + the -`pkg/all/deno` WASM bundle), it compares their mtimes against the crate sources -feeding them and **aborts (exit 1)** if any is stale, or missing. The build-first -tasks (`bench`, `corpus:compare:format`) rebuild first, so they pass the guard -for free. +The rebuild-skipping tasks (`bench:deno:run` / `bench:node:run`, +`corpus:compare:format:run`, and `smoke`) skip the rebuild so you can iterate on +the harness without paying the wasm-pack cost — at the risk of silently measuring +a binary older than current source (a CSS run once reported `146/183` against a +stale `.so` that should have been `155/183`). `lib/check_artifact_freshness.ts` +guards against this: before a run touches the executed artifacts (the runtime's +native binding + WASM bundle — Deno: FFI + `pkg/all/deno`; Node: N-API + +`pkg/all/nodejs`), it compares their mtimes against the crate sources feeding them +and **aborts (exit 1)** if any is stale, or missing. The build-first tasks +(`bench`, `bench:deno`, `bench:node`, `corpus:compare:format`) rebuild first, so +they pass the guard for free. **Escape hatch:** `BENCH_STALE_OK=1` downgrades a _stale_ artifact to a `⚠` warning and proceeds (a _missing_ one stays fatal). See the module doc for the @@ -297,9 +374,9 @@ full rationale (e.g. why stale is a hard error, not a warning). ```bash # After editing a crate, the fast/correct paths: -deno task bench # rebuilds, then runs — always fresh -deno task build:ffi && deno task bench:run # rebuild just what you changed, then :run -BENCH_STALE_OK=1 deno task bench:run # deliberately measure the current (stale) binary +deno task bench:deno # rebuilds, then runs — always fresh +deno task build:ffi && deno task bench:deno:run # rebuild just what you changed, then :run +BENCH_STALE_OK=1 deno task bench:deno:run # deliberately measure the current (stale) binary ``` ## Smoke Test @@ -308,9 +385,12 @@ BENCH_STALE_OK=1 deno task bench:run # deliberately measure the current (stal (trivial fixed inputs, non-throwing + non-empty + idempotent). Exits non-zero on any failure. Use it to catch "implementation totally broken" before running the full bench. `corpus_compare_format` is still the real correctness gate. +It is runtime-neutral like the bench: `smoke` (Deno), `smoke:node`, and +`smoke:bun` each load that runtime's own native + WASM artifacts, so an +impl-load break is caught per runtime (it's how the Bun biome-load issue surfaced). Like the bench/corpus `:run` tasks, `smoke` skips the rebuild for speed and is -guarded by the freshness check above — it aborts on a stale or missing FFI/WASM +guarded by the freshness check above — it aborts on a stale or missing native/WASM artifact (rebuild with `deno task build:bench`, or `BENCH_STALE_OK=1` to override). It is **not** a build-first task. @@ -386,8 +466,8 @@ Things the published numbers measure that aren't quite what they look like: layer) or `@fuzdev/tsv_parse_wasm` (~1.7 MB, no printers). The Binary Sizes table lists all three; the throughput rows reflect the full build. The native `tsv` row is the same story: the perf row loads the full - `libtsv_ffi`, while the Binary Sizes table also lists `tsv format (native)` - and `tsv parse (native)` subset builds (no perf rows of their own — they + `libtsv_ffi`, while the Binary Sizes table also lists `tsv format (ffi)` + and `tsv parse (ffi)` subset builds (no perf rows of their own — they exist only to size scope-matched against `oxfmt` and `oxc-parser`). - **Intersection-corpus iteration (default)** — within each group, every impl is timed on the same all-N intersection: the set of files every impl @@ -396,7 +476,7 @@ Things the published numbers measure that aren't quite what they look like: as "A is N× faster than B on the files they both handle"). Trade-off: one noisy impl shrinks the corpus for the whole group — e.g. if `biome-wasm` skips 60% of CSS files, `tsv`/`prettier`/`oxfmt` are timed on only the - remaining 40%. The Coverage section in `report.md` still discloses each + remaining 40%. The Coverage section in `report..md` still discloses each impl's preflight skip rate, so the asymmetry is visible to the reader even though the timed numbers normalize over it. `Throughput` and the `(Mf)` annotation in every table reflect the iterated set, not the full @@ -426,7 +506,7 @@ Things the published numbers measure that aren't quite what they look like: - Default off because the published ratios should reflect what users see in real code (opportunistic GC). Enable via `BENCH_GC=1` if you want the stability of forced GC for a noisy high-allocation workload. - - A `report.md` generated with the hook on has a narrower + - A `report..md` generated with the hook on has a narrower internal-vs-JSON-materializing-parser spread than a default (hook-off) run, so don't diff numbers across the two configurations line-for-line. - **`-json` parse rows are apples-to-apples; the `oxc-parser` "lazy" story @@ -492,12 +572,17 @@ and `skip` overrides. Non-existent paths are silently skipped. ## Architecture ``` -benches/deno/ -├── bench.ts # Benchmark entry point -├── smoke.ts # Smoke test for formatters and parsers -├── corpus_compare_format.ts # Formatting comparison vs prettier (entry point) -├── corpus_compare_parse.ts # Parse/AST comparison vs canonical parsers (entry point) -├── divergence_audit.ts # Divergence audit entry point +benches/js/ +├── package.json # npm dep source of truth (both runtimes); install_deps drives it +├── package-lock.json # npm lock (committed for reproducibility) +├── deno.json # nodeModulesDir: manual + jsr-only imports (@std/fs) + fmt +├── install_deps.ts # `bench:install`: npm install + force-fetch the oxc wasi binding +├── bench.ts # Benchmark entry point (runtime-neutral — runs under Deno AND Node) +├── smoke.ts # Smoke test for formatters and parsers (runtime-neutral: smoke / smoke:node / smoke:bun) +├── compose_reports.ts # Fold report.{deno,node,bun}.json → combined report.{json,md} (bench:compose) +├── corpus_compare_format.ts # Formatting comparison vs prettier (Deno-only entry point) +├── corpus_compare_parse.ts # Parse/AST comparison vs canonical parsers (Deno-only entry point) +├── divergence_audit.ts # Divergence audit entry point (Deno-only) ├── diagnostics/ # ad-hoc diagnostic scripts (not wired into `deno task` — see §Diagnostic scripts) │ ├── skip_triage.ts # parse-gap triage (tsv vs canonical) │ ├── test262_compare.ts # test262 differential (tsv vs oxc-parser, from the Rust manifest) @@ -511,16 +596,18 @@ benches/deno/ │ ├── biome.ts # Biome WASM wrapper (Svelte, TypeScript, CSS) │ ├── canonical.ts # Prettier + Svelte parser wrappers │ ├── compare_cli.ts # Shared scaffolding for the corpus_compare_* entry points -│ ├── corpus.ts # DevReposLoader + DirectoryLoader (load/stream) +│ ├── corpus.ts # DevReposLoader + DirectoryLoader (load/stream; node: builtins) │ ├── diff.ts # Line-based diff utilities (LCS algorithm) -│ ├── ffi.ts # Deno.dlopen bindings (NativeImplementation) -│ ├── implementations.ts # Implementation registry and task generation +│ ├── ffi.ts # Deno.dlopen bindings (NativeImplementation — Deno native, runtime-specific) +│ ├── napi.ts # process.dlopen bindings (NapiImplementation — Node/Bun native, runtime-specific) +│ ├── runtime.ts # Tiny cross-runtime helpers: current_runtime / os / arch normalizers +│ ├── implementations.ts # Implementation registry (branches native FFI vs N-API by runtime) │ ├── oxc.ts # OXC native wrappers (oxc-parser + oxfmt) -│ ├── oxc_wasm.ts # OXC WASM wrapper (oxc-parser via wasm32-wasi) +│ ├── oxc_wasm.ts # OXC WASM wrapper (oxc-parser via wasm32-wasi; per-runtime wasi entry) │ ├── report.ts # Summary report generation │ ├── types.ts # Shared type definitions -│ ├── versions.ts # Version loading from deno.json -│ ├── wasm.ts # WASM module loader (WasmImplementation) +│ ├── versions.ts # Version loading from package.json +│ ├── wasm.ts # WASM module loader (WasmImplementation — deno/nodejs target by runtime) │ └── divergence/ # Divergence detection module │ ├── mod.ts # Main exports │ ├── safety.ts # Safety check (differential char-frequency vs prettier) @@ -531,38 +618,43 @@ benches/deno/ ## Implementations -Versions read automatically from `deno.json` import map at runtime. +Versions read automatically from `package.json` `dependencies` at runtime +(`lib/versions.ts`). ### Updating dependencies -**How resolution works on any machine.** The `benches/deno/deno.json` import -map pins exact versions and `benches/deno/deno.lock` pins their integrity -hashes. `deno task bench` (and `smoke`, `corpus:compare:format`, `test:deno`) fetch -**exactly** those versions from npm/jsr on first run and cache them — there is -no `npm install`, no `node_modules`, and no auto-upgrade. A fresh checkout on -another machine reproduces the pinned set byte-for-byte; it never pulls newer -releases on its own. The Rust artifacts the bench builds (`tsv_ffi`, -`tsv_wasm`) are pinned the same way via `Cargo.lock`. Upgrading is always a -deliberate, committed act. +**How resolution works on any machine.** `benches/js/package.json` pins the npm +dep versions (the single source of truth, consumed by both runtimes) and +`package-lock.json` pins their integrity. **Run `deno task bench:install`** to +populate `node_modules` (one installer — `npm install` — plus the force-fetch of +the oxc wasi binding; see [Cross-Runtime](#cross-runtime-deno--node--bun)). Deno reads +that `node_modules` via `"nodeModulesDir": "manual"`; Node reads it directly. +Only the jsr-only `@std/fs` (Deno-only diagnostics) stays in the `deno.json` +import map, locked by `deno.lock`. The Rust artifacts the bench builds (`tsv_ffi`, +`tsv_napi`, `tsv_wasm`) are pinned via `Cargo.lock`. Upgrading is always a +deliberate, committed act. A plain `npm install` prunes the oxc wasi binding — +re-run `bench:install`. **Routine refresh** (alternative impls + infra — no fixture impact): ```bash -deno outdated # run in benches/deno/ — shows current vs latest -deno update --latest oxc-parser oxfmt @biomejs/js-api @biomejs/wasm-bundler @fuzdev/fuz_util zod @std/fs -deno task smoke # confirm every impl still loads + formats (32 checks) -deno task bench # regenerate results/report.{json,md} -# commit deno.json + deno.lock + results/report.* +cd benches/js && npm outdated # shows current vs latest +# bump the version in benches/js/package.json, then: +deno task bench:install # re-install at the new pins (+ re-fetch the oxc wasi binding) +deno task smoke # confirm every impl still loads + formats (32 checks) +deno task bench # regenerate report.deno.* + report.node.* +# commit package.json + package-lock.json + deno.lock + results/report.* ``` These packages are free to bump independently — they're measured against, not baked into fixtures. -⚠ **The oxc wasm binding has a subpath import entry** -(`@oxc-parser/binding-wasm32-wasi/.../parser.wasi-browser.js`) that -`deno update` does **not** bump — set it by hand to match `oxc-parser`'s -version and re-run `deno install`. `binary_sizes.ts` keys the wasm size off the -oxc version, so a mismatch silently drops the WASM size from the report. +⚠ **The oxc wasm binding is not a regular dep.** It's pure-wasm but its metadata +declares `cpu: wasm32`, so it lives in neither `dependencies` nor +`optionalDependencies` (both break or get pruned). `install_deps.ts` force-fetches +it at the `oxc-parser` version (oxc ships all bindings in lockstep) — so bumping +`oxc-parser` in `package.json` automatically carries it. `binary_sizes.ts` reads +it from `node_modules` (flat, no version dir). **Canonical baseline is coupled — do NOT bump it as routine.** The five canonical packages (`prettier`, `svelte`, `acorn`, `@sveltejs/acorn-typescript`, @@ -572,7 +664,8 @@ fixture's `expected.json` and `output_prettier.svelte`. The two pin sets **must stay identical**: the bench has to measure against the same parser/formatter that defines fixture correctness. Bumping any of the five is therefore not a benchmark refresh — it re-baselines the entire fixture corpus. Do it -deliberately: edit `deno.json` and `sidecar.ts` in lockstep, run +deliberately: edit `package.json` and `sidecar.ts` in lockstep (the +`//canonical-sync` note in package.json restates this), run `deno task fixtures:update`, and review the resulting fixture churn. ### Canonical (JS baseline) @@ -599,7 +692,7 @@ corpus divergences against `@ryanatkn` code that tsv was formatting correctly. ### OXC Package Details -**oxc-parser** (version pinned in `deno.json`) ships three package types: +**oxc-parser** (version pinned in `package.json`) ships three package types: - **Main** (`oxc-parser`): JS wrapper with platform detection. Contains `src-js/wasm.js` entry point for direct WASM usage. Supports `NAPI_RS_FORCE_WASI` env var to force WASM. @@ -609,7 +702,7 @@ corpus divergences against `@ryanatkn` code that tsv was formatting correctly. optional dependency of main package — ships alongside native, not as a separate product. Depends on `@napi-rs/wasm-runtime` → `@emnapi/runtime`, `@emnapi/core`, `@tybys/wasm-util`. -**oxfmt** (version pinned in `deno.json`) ships native bindings only: +**oxfmt** (version pinned in `package.json`) ships native bindings only: - **Main** (`oxfmt`): JS wrapper bundling Prettier internals. Depends on `tinypool`. - **Native bindings** (`@oxfmt/{platform}`): 8 platform variants. **No WASM variant exists.** @@ -654,25 +747,28 @@ counts make this immediately visible without needing `--verbose`. Benchmark output includes binary/WASM size comparison across implementations: -- **`tsv`**: native FFI (`.so`/`.dylib`/`.dll`) and WASM (`.wasm`) from build output. - Native ships three rows from one `tsv_ffi` crate via its `format`/`parse` features - (matching the three WASM rows): the full `libtsv_ffi` (`target/release`, both - features — the build the perf rows load), `tsv format (native)` - (`target/ffi-format/release`, `--features format`, no convert layer — scope-matched - to `oxfmt (native)`), and `tsv parse (native)` (`target/ffi-parse/release`, - `--features parse`, printers dropped — scope-matched to `oxc-parser (native)`). - `deno task bench` builds all three; the subset rows are omitted if those builds - haven't been run. -- **biome**: WASM (`.wasm`) from Deno npm cache -- **oxc-parser**: native binding (`.node`) and WASM (`.wasm` from `binding-wasm32-wasi`) from Deno npm cache -- **oxfmt**: native binding (`.node`) from Deno npm cache (no WASM variant) +- **`tsv`**: native FFI (`.so`/`.dylib`/`.dll`), N-API addon (`.node`), and WASM + (`.wasm`) from build output. The FFI side ships three rows from one `tsv_ffi` crate + via its `format`/`parse` features (matching the three WASM rows): the full + `libtsv_ffi` (`target/release`, both features — the build the perf rows load), + `tsv format (ffi)` (`target/ffi-format/release`, `--features format`, no convert + layer — scope-matched to `oxfmt (napi)`), and `tsv parse (ffi)` + (`target/ffi-parse/release`, `--features parse`, printers dropped — scope-matched to + `oxc-parser (napi)`). `tsv (napi)` is the N-API addon (`tsv_napi`, the Node/Bun + native path). Native-kind labels name the binding (`ffi`/`napi`), not just "native", + so the row's mechanism is unambiguous. `deno task bench` builds all of them; the + subset rows are omitted if those builds haven't been run. +- **biome**: WASM (`.wasm`) from node_modules +- **oxc-parser**: N-API binding (`.node`) and WASM (`.wasm` from `binding-wasm32-wasi`) from node_modules +- **oxfmt**: N-API binding (`.node`) from node_modules (no WASM variant) Each row reports **raw on-disk size** plus **gzipped size** (≈ npm-tarball wire size). Sizes are grouped by kind (WASM vs native) with ratios relative to `tsv` shown for both raw and gzipped. Gzipped column shows `—` when `gzip` isn't on PATH (e.g., bare Windows); raw size still -collects fine. `bench:run` needs `--allow-run=git,gzip` for the -subprocess. +collects fine. `bench:deno:run` needs `--allow-run=git,gzip` for the +subprocess (Node needs no permission flags). gzip runs via `node:child_process` +`execFile` (portable across both runtimes). Compression mechanism is `gzip -c` (system default level 6), matching `scripts/patch_npm_package.ts`. Level 6 corresponds to what @@ -681,10 +777,10 @@ numbers cited in some perf-doc histories used `gzip -9` and run ~2-3% smaller — both are recorded in `docs/performance.md` for the WASM binaries. -JSON output (`results/report.json`) gains a per-entry `gzip_bytes: +JSON output (`results/report..json`) gains a per-entry `gzip_bytes: number | null` field alongside the existing `bytes`. -Combined `oxc-parser+oxfmt (native)` row sums both raw and gzipped +Combined `oxc-parser+oxfmt (napi)` row sums both raw and gzipped sizes from the parts. The gzipped sum slightly overstates wire size because the streams don't share a dictionary, but it matches npm's two-tarball reality. @@ -728,7 +824,7 @@ Implementation: `lib/binary_sizes.ts` printer/crate can't lose content in unrelated languages. See `lib/ffi.ts` for the Deno-FFI corpus heisenbug. - **Parse benchmark overhead**: JSON materialization, not parsing, dominates - the `-json` rows (see `results/report.md` for the current per-language + the `-json` rows (see `results/report..md` for the current per-language ratios). Use `tsv-internal` for raw parse speed. Both the native and WASM rows go through `convert_ast_json_string`, which skips the intermediate `serde_json::Value` when eligible (per-language eligibility: @@ -754,13 +850,13 @@ Implementation: `lib/binary_sizes.ts` These live under `diagnostics/`. The parser-analysis ones (`comment_dup_scan`, `acorn_dup_fuzz`) need the canonical-parser import map, so pass `--config -benches/deno/deno.json`; all run from the repo root (corpus/artifact paths are +benches/js/deno.json`; all run from the repo root (corpus/artifact paths are CWD-relative). - `diagnostics/skip_triage.ts` — parse every corpus file with tsv + the canonical parser, bucket into tsv-fails-canonical-ok / canonical-fails-tsv-ok / both-fail. Run: - `deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys benches/deno/diagnostics/skip_triage.ts` + `deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys benches/js/diagnostics/skip_triage.ts` - `diagnostics/test262_compare.ts` — test262 differential conformance, tsv vs oxc-parser. Consumes the manifest from `tsv_debug test262 --emit-manifest ` (tsv's graded strict subset + each test's expected/tsv verdict), runs oxc over the same files as a module (mirroring tsv's module-only @@ -768,21 +864,21 @@ CWD-relative). oxc accepts) and negative **early-error gaps** (oxc rejects, tsv accepts). On-demand triage, not a CI gate; numbers move with the pinned oxc version. No biome (its js-api has no parser to grade). See `docs/conformance_test262.md` §Differential. Run from the repo root: - `cargo run -p tsv_debug test262 --emit-manifest /tmp/t262.json && deno run --allow-read --allow-env --allow-ffi --allow-net --allow-sys --config benches/deno/deno.json benches/deno/diagnostics/test262_compare.ts --manifest /tmp/t262.json` + `cargo run -p tsv_debug test262 --emit-manifest /tmp/t262.json && deno run --allow-read --allow-env --allow-ffi --allow-net --allow-sys --config benches/js/deno.json benches/js/diagnostics/test262_compare.ts --manifest /tmp/t262.json` - `diagnostics/comment_dup_scan.ts` — comment-duplication fixture-corpus completeness guard. Walks all fixtures with two oracles (live `svelte/compiler` parse + committed expected JSON), flagging any comment span emitted ≥2× within one array (the acorn backtrack-reparse signature tsv corrects to single). RED buckets must stay empty. Re-run after touching the comment-convert layer or bumping `@sveltejs/acorn-typescript`. Run: - `deno run --allow-read --allow-env --allow-net --allow-sys --config benches/deno/deno.json benches/deno/diagnostics/comment_dup_scan.ts` + `deno run --allow-read --allow-env --allow-net --allow-sys --config benches/js/deno.json benches/js/diagnostics/comment_dup_scan.ts` - `diagnostics/acorn_dup_fuzz.ts` — fuzzes a comment into every position of acorn-typescript's own ~200 construct test inputs and flags any `onComment` double-fire; the broadest net for an un-enumerated duplicating construct, and the upstream-fix validation harness (a correct A+B patch drops the count to 0). Default reads `~/dev/acorn-typescript-fork/test`; pass a path to override. Run: - `deno run --allow-read --allow-env --allow-net --allow-sys --config benches/deno/deno.json benches/deno/diagnostics/acorn_dup_fuzz.ts` + `deno run --allow-read --allow-env --allow-net --allow-sys --config benches/js/deno.json benches/js/diagnostics/acorn_dup_fuzz.ts` - `diagnostics/wasm_json_probe.ts` — split parse cost into pure-parse vs materialization for native + WASM, isolating JS-side `JSON.parse`. - `diagnostics/wasm_format_probe.ts` — measure WASM **format** wall-time at the resolution diff --git a/benches/deno/bench.ts b/benches/js/bench.ts similarity index 88% rename from benches/deno/bench.ts rename to benches/js/bench.ts index 608b26937..d494d5e6c 100644 --- a/benches/deno/bench.ts +++ b/benches/js/bench.ts @@ -10,18 +10,20 @@ * - WASM: tsv compiled to WASM (portable, near-native) * - Alternatives: oxc-parser, oxfmt, biome-wasm (for comparison) * - * Run with: deno task bench:run + * Run with: deno task bench:deno:run (Deno) or deno task bench:node:run (Node). + * The same body runs under both — it detects the runtime and writes a + * runtime-labeled report (report.deno.* / report.node.*). See benches/js/CLAUDE.md. * * CLI options: * --json Output results as JSON * --markdown Output results as Markdown * --save-baseline Also save results as baseline for regression detection * --compare-baseline Compare against saved baseline - * --save-report Overwrite the canonical report.{json,md} even on a limited run + * --save-report Overwrite the canonical report..{json,md} even on a limited run * --verbose Include per-file skip detail (paths + errors + failure sets) * - * Results are always saved to benches/deno/results/_.{json,md}. - * Latest results are also written to benches/deno/results/report.{json,md} (committed to git). + * Results are always saved to benches/js/results/_..{json,md}. + * Latest results are also written to benches/js/results/report..{json,md} (committed to git). * * Environment variables: * BENCH_LIMIT Limit files per language (default: all) @@ -48,6 +50,10 @@ import { benchmark_baseline_format, benchmark_baseline_save, } from '@fuzdev/fuz_util/benchmark_baseline.ts'; +import { spawn_out } from '@fuzdev/fuz_util/process.ts'; +import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'; +import { argv, env, exit } from 'node:process'; +import { fileURLToPath } from 'node:url'; import { DevReposLoader, group_by_language } from './lib/corpus.ts'; import { canonical_parser_label, @@ -80,6 +86,14 @@ import { import { type Language, LANGUAGES, type SourceFile } from './lib/types.ts'; import { check_artifact_freshness, wasm_artifact_path } from './lib/check_artifact_freshness.ts'; import { get_library_path } from './lib/ffi.ts'; +import { get_napi_library_path } from './lib/napi.ts'; +import { current_runtime, type Runtime } from './lib/runtime.ts'; + +/** The JS runtime executing this bench — labels the report siblings + * (`report.deno.*` / `report.node.*`) and every row's `runtime` field, and + * selects the runtime-specific native (FFI vs N-API) + WASM (deno vs nodejs + * target) artifacts below. The same bench body runs under both. */ +const RUNTIME = current_runtime(); // // CLI Arguments @@ -95,8 +109,10 @@ const Args_schema = z.strictObject({ verbose: z.boolean().default(false), }); -// Strip leading -- from deno task passthrough -const raw_argv = Deno.args[0] === '--' ? Deno.args.slice(1) : Deno.args; +// Strip leading -- from deno task passthrough. `argv.slice(2)` (node:process) is +// the cross-runtime equivalent of `Deno.args` — Deno exposes the same shape. +const cli_args = argv.slice(2); +const raw_argv = cli_args[0] === '--' ? cli_args.slice(1) : cli_args; const parsed_argv = argv_parse(raw_argv); const parsed = args_parse(parsed_argv, Args_schema); @@ -109,12 +125,12 @@ if (!parsed.success) { parsed.error.issues.map((i: { message: string }) => i.message).join(', '), ); console.error(`Known flags: ${known.join(', ')}`); - Deno.exit(1); + exit(1); } if (parsed.data._.length > 0) { console.error(`Unexpected positional arguments: ${parsed.data._.join(', ')}`); - Deno.exit(1); + exit(1); } const args = { @@ -183,7 +199,7 @@ console.error = (...args: unknown[]): void => { /** Parse optional non-negative integer from env var; malformed values fall back to undefined. */ const env_int = (name: string): number | undefined => { - const val = Deno.env.get(name); + const val = env[name]; if (!val) return undefined; const n = parseInt(val, 10); return Number.isFinite(n) && n >= 0 ? n : undefined; @@ -193,7 +209,7 @@ const env_int = (name: string): number | undefined => { const MAX_FILES_PER_LANGUAGE = env_int('BENCH_LIMIT'); /** Filter files by path pattern (default: none) */ -const FILE_FILTER = Deno.env.get('BENCH_FILTER'); +const FILE_FILTER = env.BENCH_FILTER; /** Duration per benchmark in ms (default: 5000) */ const BENCH_DURATION = env_int('BENCH_DURATION') ?? 5000; @@ -208,7 +224,7 @@ const BENCH_WARMUP = env_int('BENCH_WARMUP') ?? 3; * stabilizing high-allocation workloads at the cost of penalizing efficient * low-allocation paths. See `CLAUDE.md` → Fairness Caveats for the trade-off. */ -const BENCH_GC = Deno.env.get('BENCH_GC') === '1'; +const BENCH_GC = env.BENCH_GC === '1'; /** * Include the `tsv-forced-async` control row (default off). Same native engine @@ -218,7 +234,7 @@ const BENCH_GC = Deno.env.get('BENCH_GC') === '1'; * report and the regression baseline; set `BENCH_FORCED_ASYNC=1` to enable. * See `BenchmarkTaskOptions.forced_async`. */ -const BENCH_FORCED_ASYNC = Deno.env.get('BENCH_FORCED_ASYNC') === '1'; +const BENCH_FORCED_ASYNC = env.BENCH_FORCED_ASYNC === '1'; /** * Iteration corpus mode. Default `intersection`: within each group, every @@ -232,10 +248,10 @@ const BENCH_FORCED_ASYNC = Deno.env.get('BENCH_FORCED_ASYNC') === '1'; * useful for reproducing pre-intersection numbers or auditing what the * intersection mode hides. */ -const BENCH_MODE = Deno.env.get('BENCH_MODE'); +const BENCH_MODE = env.BENCH_MODE; if (BENCH_MODE !== undefined && BENCH_MODE !== 'intersection' && BENCH_MODE !== 'union') { console.error(`Invalid BENCH_MODE: ${BENCH_MODE}. Expected 'intersection' or 'union'.`); - Deno.exit(1); + exit(1); } const USE_INTERSECTION = BENCH_MODE !== 'union'; @@ -246,15 +262,15 @@ const MAX_ERROR_MESSAGE_LENGTH = 200; * Baseline storage directory. Passed to `benchmark_baseline_save` / * `_compare`; the library calls `mkdir(path, { recursive: true })` and * writes `baseline.json` inside, so the file lands at - * `./benches/deno/results/baseline.json`. Moved into `results/` (from its - * pre-0.60 location at `./benches/deno/baseline.json`) so the library's - * mkdir is covered by the existing `--allow-write=benches/deno/results` + * `./benches/js/results/baseline.json`. Moved into `results/` (from its + * pre-0.60 location at `./benches/js/baseline.json`) so the library's + * mkdir is covered by the existing `--allow-write=benches/js/results` * permission without widening write scope to the whole benches tree. */ -const BASELINE_DIR = './benches/deno/results'; +const BASELINE_DIR = './benches/js/results'; /** Results directory for comparison JSON files */ -const RESULTS_DIR = './benches/deno/results'; +const RESULTS_DIR = './benches/js/results'; // // Setup @@ -335,24 +351,50 @@ log( log(` Total: ${String(total_files).padEnd(11)} files (${fmt_bytes(total_bytes)})`); log(); -// Refuse to measure stale binaries (bench:run skips the rebuild). See -// lib/check_artifact_freshness.ts; override with BENCH_STALE_OK=1. -const ffi_profile = Deno.env.get('TSV_FFI_PROFILE') ?? 'release'; -await check_artifact_freshness([ - { - label: `FFI (${ffi_profile})`, +// Refuse to measure stale binaries (the `:run` tasks skip the rebuild). See +// lib/check_artifact_freshness.ts; override with BENCH_STALE_OK=1. The native + +// WASM artifacts are runtime-specific: Deno executes the FFI library + the +// `deno`-target WASM bundle; Node/Bun execute the N-API addon + the `nodejs` +// target. `wasm_artifact_path` resolves the runtime's bundle itself. +const native_check = RUNTIME === 'deno' + ? { + label: `FFI (${env.TSV_FFI_PROFILE ?? 'release'})`, path: get_library_path(), binding_crates: ['tsv_ffi'], rebuild: 'deno task build:ffi', - }, + } + : { + label: 'N-API', + path: get_napi_library_path(), + binding_crates: ['tsv_napi'], + rebuild: 'deno task build:napi', + }; +const wasm_target = RUNTIME === 'deno' ? 'deno' : 'nodejs'; +await check_artifact_freshness([ + native_check, { - label: 'WASM (all/deno)', + label: `WASM (all/${wasm_target})`, path: wasm_artifact_path('all'), binding_crates: ['tsv_wasm'], - rebuild: 'deno task build:wasm:all:deno', + rebuild: `deno task build:wasm:all:${wasm_target}`, }, ]); +// Friendly preflight: the canonical impls (prettier + svelte/compiler) resolve +// from the harness `node_modules`; without it, init fails with an opaque +// module-resolution error. Point the user at the one installer instead. (Cheap +// existence check — `bench:install` populates the tree both runtimes consume.) +const node_modules_path = fileURLToPath(new URL('./node_modules', import.meta.url)); +try { + await stat(node_modules_path); +} catch { + console.error( + `\n✗ Harness dependencies not installed (${node_modules_path} missing).\n` + + ` Run 'deno task bench:install' first — it installs the npm deps + the oxc wasi binding.\n`, + ); + exit(1); +} + // Initialize implementations const impls = await init_implementations({ logger: log }); @@ -605,7 +647,7 @@ async function run_benchmark_group( // fires and then all subsequent timers stall forever. The default // 100ms inter-task cooldown is the only timer-dependent await in // the loop, so dropping it sidesteps the hang. - // See benches/deno/CLAUDE.md → Known Issues. + // See benches/js/CLAUDE.md → Known Issues. cooldown_ms: 0, on_iteration: BENCH_GC ? () => globalThis.gc?.() : undefined, on_task_complete: (result: BenchmarkResult, index: number, total: number) => { @@ -713,6 +755,14 @@ interface BaselineEntry { * reflect. `null` when tracking is unavailable. */ files_iterated: number | null; + /** + * The JS runtime that produced this row (`deno` | `node` | `bun`). Every row + * carries it so a reader never has to guess what produced a number — the + * runtime-labeled sibling reports (`report.deno.*` / `report.node.*`) compose + * at the display layer (tsv.fuz.dev), and a per-runtime delta on the same row + * is the detector for a runtime-specific measurement artifact. + */ + runtime: Runtime; } /** Package versions used in the benchmark run */ @@ -731,6 +781,9 @@ interface BaselineVersions { interface Baseline { version: number; + /** The JS runtime that produced this report (`deno` | `node` | `bun`). Mirrors + * the per-row `runtime` and matches the `report..{json,md}` filename. */ + runtime: Runtime; timestamp: string; git_commit: string | null; corpus: { @@ -759,8 +812,8 @@ interface Baseline { */ async function get_tsv_version(): Promise { try { - const cargo_toml_path = new URL('../../Cargo.toml', import.meta.url).pathname; - const content = await Deno.readTextFile(cargo_toml_path); + const cargo_toml_path = fileURLToPath(new URL('../../Cargo.toml', import.meta.url)); + const content = await readFile(cargo_toml_path, 'utf8'); // Match the line-leading `version = "..."` inside the `[workspace.package]` section. // `^version` (multiline) avoids matching a `rust-version = "..."` MSRV pin; `[^[]*?` // bounds the search to the section by stopping at the next `[` heading. @@ -775,14 +828,9 @@ async function get_tsv_version(): Promise { /** Get current git commit hash */ async function get_git_commit(): Promise { try { - const cmd = new Deno.Command('git', { - args: ['rev-parse', 'HEAD'], - stdout: 'piped', - stderr: 'null', - }); - const output = await cmd.output(); - if (output.success) { - return new TextDecoder().decode(output.stdout).trim().slice(0, 8); + const { result, stdout } = await spawn_out('git', ['rev-parse', 'HEAD']); + if (result.ok && stdout) { + return stdout.trim().slice(0, 8); } } catch { // Ignore @@ -824,12 +872,15 @@ async function build_results_data( files_processed: coverage?.processed ?? null, files_total: coverage?.total ?? null, files_iterated: iterated ?? null, + runtime: RUNTIME, }); } } return { - version: 4, + // Bumped 4 → 5 for the added `runtime` field (top-level + per row). + version: 5, + runtime: RUNTIME, timestamp: new Date().toISOString(), git_commit: await get_git_commit(), corpus, @@ -863,6 +914,7 @@ function generate_markdown_report( const lines: string[] = []; lines.push('# tsv benchmark results\n'); const commit_str = git_commit ? ` (${git_commit})` : ''; + lines.push(`**Runtime:** ${RUNTIME}\n`); lines.push(`**Date:** ${timestamp} — tsv ${versions.tsv}${commit_str}\n`); const total_files = corpus.svelte + corpus.typescript + corpus.css; @@ -976,9 +1028,11 @@ function generate_markdown_report( * Save results to the results directory. * * Always writes a timestamped pair. Only overwrites the canonical - * `report.{json,md}` when `write_report` is true — gated by the caller so - * that partial runs (BENCH_LIMIT, BENCH_FILTER) don't clobber the - * committed canonical report. + * `report..{json,md}` when `write_report` is true — gated by the caller + * so that partial runs (BENCH_LIMIT, BENCH_FILTER) don't clobber the committed + * canonical report. Every filename is runtime-suffixed so a Deno run and a Node + * run write sibling files (`report.deno.*` / `report.node.*`) instead of one + * clobbering the other. */ async function save_results( data: Baseline, @@ -986,10 +1040,10 @@ async function save_results( binary_sizes: BinarySize[], write_report: boolean, ): Promise { - await Deno.mkdir(RESULTS_DIR, { recursive: true }); + await mkdir(RESULTS_DIR, { recursive: true }); const timestamp = data.timestamp.replace(/[:.]/g, '-').slice(0, 19); const commit = data.git_commit ?? 'unknown'; - const base_path = `${RESULTS_DIR}/${timestamp}_${commit}`; + const base_path = `${RESULTS_DIR}/${timestamp}_${commit}.${RUNTIME}`; const markdown = generate_markdown_report( groups, @@ -1008,13 +1062,13 @@ async function save_results( const json = JSON.stringify(data, null, '\t'); const writes: Promise[] = [ - Deno.writeTextFile(`${base_path}.json`, json), - Deno.writeTextFile(`${base_path}.md`, markdown), + writeFile(`${base_path}.json`, json), + writeFile(`${base_path}.md`, markdown), ]; if (write_report) { writes.push( - Deno.writeTextFile(`${RESULTS_DIR}/report.json`, json), - Deno.writeTextFile(`${RESULTS_DIR}/report.md`, markdown), + writeFile(`${RESULTS_DIR}/report.${RUNTIME}.json`, json), + writeFile(`${RESULTS_DIR}/report.${RUNTIME}.md`, markdown), ); } await Promise.all(writes); @@ -1074,7 +1128,7 @@ async function save_baseline(data: Baseline): Promise { * detection for per-task budget drift, and OR-gated noise warnings on * high-cv or high-outlier-ratio rows. The flat ±5% ops/sec gate that lived * here previously is gone — see `benchmark_baseline_compare` and the - * fairness caveats in benches/deno/CLAUDE.md. + * fairness caveats in benches/js/CLAUDE.md. */ async function compare_baseline(current: Baseline): Promise { const comparison = await benchmark_baseline_compare( @@ -1131,8 +1185,10 @@ async function compare_baseline(current: Baseline): Promise { // Output // -// Collect binary sizes once (used by all output paths) -const binary_sizes = await collect_binary_sizes(impls.versions, { +// Collect binary sizes once (used by all output paths). Versions no longer +// thread through — bindings live in node_modules (flat, no version dir). The +// tsv napi `.node` row is existence-gated (omitted when unbuilt), so no flag. +const binary_sizes = await collect_binary_sizes({ has_native: !!impls.native, has_wasm: !!impls.wasm, has_oxc: !!impls.oxc, @@ -1231,7 +1287,7 @@ if (suppressed_noise.size > 0) { } // Always save the timestamped pair; only overwrite the canonical -// `report.{json,md}` on full-corpus runs or when --save-report is set. +// `report..{json,md}` on full-corpus runs or when --save-report is set. const write_report = args.save_report || !is_limited; const results_path = await save_results( results_data, @@ -1244,8 +1300,8 @@ log(` ${results_path}.json`); log(` ${results_path}.md`); if (write_report) { log(`Canonical report updated:`); - log(` ${RESULTS_DIR}/report.json`); - log(` ${RESULTS_DIR}/report.md`); + log(` ${RESULTS_DIR}/report.${RUNTIME}.json`); + log(` ${RESULTS_DIR}/report.${RUNTIME}.md`); } else { log(`Skipped canonical report (limited run — pass --save-report to override)`); } diff --git a/benches/js/compose_reports.ts b/benches/js/compose_reports.ts new file mode 100644 index 000000000..0ce9e57f1 --- /dev/null +++ b/benches/js/compose_reports.ts @@ -0,0 +1,185 @@ +/** + * Compose the per-runtime sibling reports (`report.deno.json` / `report.node.json` + * / `report.bun.json`) into ONE compact cross-runtime view: + * `results/report.{json,md}`. + * + * Deliberately NOT a verbose triplicate — the full per-runtime reports stay as + * the `report..{json,md}` siblings. This emits only the cross-runtime + * comparison: per `(group, impl)` row, each runtime's ops/sec side by side plus + * the ratio vs the first present runtime. A per-runtime delta on the same row is + * the signal worth reading — same engine, different runtime + binding boundary + * (Deno → FFI, Node/Bun → N-API) — and the whole reason the bench runs under + * multiple runtimes (see benches/js/CLAUDE.md §Cross-Runtime). This is also + * what tsv.fuz.dev composes at the display layer. + * + * Runs whatever subset of reports exists (a missing runtime is skipped, not an + * error — unless none exist). Portable (`node:` builtins) — runs under any + * runtime. + * + * Run: deno task bench:compose (after one or more runtimes' reports exist) + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { exit } from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const RUNTIMES = ['deno', 'node', 'bun'] as const; +type Runtime = (typeof RUNTIMES)[number]; + +/** The fields of a per-runtime `report..json` row this composer reads. */ +interface Entry { + name: string; + group: string; + mean_ns: number; + ops_per_second: number; + files_iterated: number | null; + runtime: Runtime; +} + +interface Report { + version: number; + runtime: Runtime; + timestamp: string; + git_commit: string | null; + versions: Record; + entries: Entry[]; +} + +const results_dir = fileURLToPath(new URL('./results/', import.meta.url)); + +async function read_report(runtime: Runtime): Promise { + try { + return JSON.parse(await readFile(`${results_dir}report.${runtime}.json`, 'utf8')) as Report; + } catch { + return null; + } +} + +const reports = new Map(); +for (const r of RUNTIMES) { + const report = await read_report(r); + if (report) reports.set(r, report); +} + +if (reports.size === 0) { + console.error( + 'No per-runtime reports found in results/ ' + + '(report.deno.json / report.node.json / report.bun.json).\n' + + 'Run `deno task bench` (or a per-runtime bench) first.', + ); + exit(1); +} + +/** Runtimes present, in canonical order; the first is the ratio baseline. */ +const present = RUNTIMES.filter((r) => reports.has(r)); +const base_runtime = present[0]; + +/** One cross-runtime comparison row, keyed by `${group}/${name}`. */ +interface Row { + group: string; + name: string; + ops: Partial>; + mean_ns: Partial>; + files_iterated: Partial>; +} + +const rows = new Map(); +const order: string[] = []; +for (const r of present) { + for (const e of reports.get(r)!.entries) { + const key = `${e.group}/${e.name}`; + let row = rows.get(key); + if (!row) { + row = { group: e.group, name: e.name, ops: {}, mean_ns: {}, files_iterated: {} }; + rows.set(key, row); + order.push(key); + } + row.ops[r] = e.ops_per_second; + row.mean_ns[r] = e.mean_ns; + row.files_iterated[r] = e.files_iterated; + } +} + +// JSON: metadata + provenance per source + the comparison rows. +const combined = { + version: 5, + kind: 'combined' as const, + generated: new Date().toISOString(), + runtimes: present, + sources: present.map((r) => ({ + runtime: r, + timestamp: reports.get(r)!.timestamp, + git_commit: reports.get(r)!.git_commit, + tsv: reports.get(r)!.versions?.tsv ?? null, + })), + rows: order.map((key) => { + const row = rows.get(key)!; + return { + group: row.group, + name: row.name, + ops_per_second: row.ops, + mean_ns: row.mean_ns, + files_iterated: row.files_iterated, + }; + }), +}; + +// Markdown: one table per group, runtimes side by side + ratio vs base_runtime. +function fmt_ops(n: number | undefined): string { + return n === undefined ? '—' : n.toFixed(1); +} + +function fmt_ratio(self: number | undefined, base: number | undefined): string { + if (self === undefined || base === undefined || base === 0) return '—'; + return `${(self / base).toFixed(2)}x`; +} + +const others = present.filter((r) => r !== base_runtime); + +const md: string[] = []; +md.push('# tsv benchmark results — cross-runtime\n'); +md.push(`**Generated:** ${combined.generated}\n`); +md.push( + `**Runtimes:** ${present.join(', ')} ` + + '— each runtime’s full report is its `report..{json,md}` sibling.\n', +); +md.push( + 'A per-runtime delta on the same row is the signal: same engine, different ' + + 'runtime + binding boundary (Deno → FFI, Node/Bun → N-API). Ratios are vs ' + + `\`${base_runtime}\` (> 1 = faster than ${base_runtime}).\n`, +); + +const groups: string[] = []; +for (const key of order) { + const g = rows.get(key)!.group; + if (!groups.includes(g)) groups.push(g); +} + +for (const group of groups) { + md.push(`## ${group}\n`); + const header = [ + 'Impl', + ...present.map((r) => `${r} ops/sec`), + ...others.map((r) => `${r}/${base_runtime}`), + ]; + md.push(`| ${header.join(' | ')} |`); + md.push(`| ${header.map((_, i) => (i === 0 ? '---' : '---:')).join(' | ')} |`); + for (const key of order) { + const row = rows.get(key)!; + if (row.group !== group) continue; + const cells = [ + row.name, + ...present.map((r) => fmt_ops(row.ops[r])), + ...others.map((r) => fmt_ratio(row.ops[r], row.ops[base_runtime])), + ]; + md.push(`| ${cells.join(' | ')} |`); + } + md.push(''); +} + +await writeFile(`${results_dir}report.json`, JSON.stringify(combined, null, '\t')); +await writeFile(`${results_dir}report.md`, md.join('\n')); + +console.log(`Composed cross-runtime report from: ${present.join(', ')}`); +console.log(` ${results_dir}report.json`); +console.log(` ${results_dir}report.md`); diff --git a/benches/deno/corpus_compare_format.ts b/benches/js/corpus_compare_format.ts similarity index 100% rename from benches/deno/corpus_compare_format.ts rename to benches/js/corpus_compare_format.ts diff --git a/benches/deno/corpus_compare_parse.ts b/benches/js/corpus_compare_parse.ts similarity index 100% rename from benches/deno/corpus_compare_parse.ts rename to benches/js/corpus_compare_parse.ts diff --git a/benches/js/deno.json b/benches/js/deno.json new file mode 100644 index 000000000..847d06c18 --- /dev/null +++ b/benches/js/deno.json @@ -0,0 +1,13 @@ +{ + "//deps": "npm dep versions are the source of truth in ./package.json (read by BOTH runtimes). Deno consumes that node_modules via `nodeModulesDir: manual` — run `npm install` here first. Only jsr-only (`@std/*`) and Deno-specific subpath specifiers belong in `imports`; everything else npm resolves bare from package.json. Canonical-sync note lives in package.json now.", + "nodeModulesDir": "manual", + "imports": { + "@std/fs": "jsr:@std/fs@^1.0.24" + }, + "fmt": { + "useTabs": true, + "lineWidth": 100, + "singleQuote": true, + "proseWrap": "preserve" + } +} diff --git a/benches/deno/deno.lock b/benches/js/deno.lock similarity index 97% rename from benches/deno/deno.lock rename to benches/js/deno.lock index 37fece885..9aab7a3ef 100644 --- a/benches/deno/deno.lock +++ b/benches/js/deno.lock @@ -7,7 +7,6 @@ "npm:@biomejs/js-api@4.0.0": "4.0.0_@biomejs+wasm-bundler@2.4.16", "npm:@biomejs/wasm-bundler@2.4.16": "2.4.16", "npm:@fuzdev/fuz_util@0.64.0": "0.64.0_svelte@5.56.1_zod@4.4.3", - "npm:@oxc-parser/binding-wasm32-wasi@0.134.0": "0.134.0", "npm:@sveltejs/acorn-typescript@1.0.10": "1.0.10_acorn@8.16.0", "npm:acorn@8.16.0": "8.16.0", "npm:oxc-parser@0.134.0": "0.134.0", @@ -516,19 +515,23 @@ }, "workspace": { "dependencies": [ - "jsr:@std/fs@^1.0.24", - "npm:@biomejs/js-api@4.0.0", - "npm:@biomejs/wasm-bundler@2.4.16", - "npm:@fuzdev/fuz_util@0.64.0", - "npm:@oxc-parser/binding-wasm32-wasi@0.134.0", - "npm:@sveltejs/acorn-typescript@1.0.10", - "npm:acorn@8.16.0", - "npm:oxc-parser@0.134.0", - "npm:oxfmt@0.53.0", - "npm:prettier-plugin-svelte@4.1.1", - "npm:prettier@3.8.3", - "npm:svelte@5.56.1", - "npm:zod@^4.4.3" - ] + "jsr:@std/fs@^1.0.24" + ], + "packageJson": { + "dependencies": [ + "npm:@biomejs/js-api@4.0.0", + "npm:@biomejs/wasm-bundler@2.4.16", + "npm:@fuzdev/fuz_util@0.64.0", + "npm:@sveltejs/acorn-typescript@1.0.10", + "npm:@types/node@24", + "npm:acorn@8.16.0", + "npm:oxc-parser@0.134.0", + "npm:oxfmt@0.53.0", + "npm:prettier-plugin-svelte@4.1.1", + "npm:prettier@3.8.3", + "npm:svelte@5.56.1", + "npm:zod@^4.4.3" + ] + } } } diff --git a/benches/deno/diagnostics/acorn_dup_fuzz.ts b/benches/js/diagnostics/acorn_dup_fuzz.ts similarity index 97% rename from benches/deno/diagnostics/acorn_dup_fuzz.ts rename to benches/js/diagnostics/acorn_dup_fuzz.ts index 92bcb125f..a4b2cc144 100644 --- a/benches/deno/diagnostics/acorn_dup_fuzz.ts +++ b/benches/js/diagnostics/acorn_dup_fuzz.ts @@ -18,7 +18,7 @@ * * Run (default reads ../test of a sibling acorn-typescript checkout; pass a path to override): * deno run --allow-read --allow-env --allow-net --allow-sys \ - * --config benches/deno/deno.json benches/deno/diagnostics/acorn_dup_fuzz.ts [TEST_DIR] + * --config benches/js/deno.json benches/js/diagnostics/acorn_dup_fuzz.ts [TEST_DIR] */ import { walk } from '@std/fs'; diff --git a/benches/deno/diagnostics/comment_dup_scan.ts b/benches/js/diagnostics/comment_dup_scan.ts similarity index 99% rename from benches/deno/diagnostics/comment_dup_scan.ts rename to benches/js/diagnostics/comment_dup_scan.ts index 1c4f438fd..4acffd5ef 100644 --- a/benches/deno/diagnostics/comment_dup_scan.ts +++ b/benches/js/diagnostics/comment_dup_scan.ts @@ -19,7 +19,7 @@ * * Run: * deno run --allow-read --allow-env --allow-net --allow-sys \ - * --config benches/deno/deno.json benches/deno/diagnostics/comment_dup_scan.ts [--json] [--verbose] + * --config benches/js/deno.json benches/js/diagnostics/comment_dup_scan.ts [--json] [--verbose] * * Only `.svelte` fixtures can carry comment dups: `.ts`/`.svelte.ts` route to * acorn-typescript with NO onComment (the sidecar never sets it), so their canonical diff --git a/benches/deno/diagnostics/skip_triage.ts b/benches/js/diagnostics/skip_triage.ts similarity index 97% rename from benches/deno/diagnostics/skip_triage.ts rename to benches/js/diagnostics/skip_triage.ts index adbd03211..5711dd048 100644 --- a/benches/deno/diagnostics/skip_triage.ts +++ b/benches/js/diagnostics/skip_triage.ts @@ -6,7 +6,7 @@ * asymmetry buckets (tsv-fails-canonical-ok = real gaps). Not wired * into `deno task` — run ad hoc (full JSON to stdout, summary to stderr): * deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys \ - * benches/deno/diagnostics/skip_triage.ts 2>/dev/null > /tmp/triage.json + * benches/js/diagnostics/skip_triage.ts 2>/dev/null > /tmp/triage.json */ import { DevReposLoader, group_by_language } from '../lib/corpus.ts'; diff --git a/benches/deno/diagnostics/test262_compare.ts b/benches/js/diagnostics/test262_compare.ts similarity index 98% rename from benches/deno/diagnostics/test262_compare.ts rename to benches/js/diagnostics/test262_compare.ts index 1c01fc269..16e3511f6 100644 --- a/benches/deno/diagnostics/test262_compare.ts +++ b/benches/js/diagnostics/test262_compare.ts @@ -17,8 +17,8 @@ * oxc native: * cargo run -p tsv_debug test262 --emit-manifest /tmp/t262.json * deno run --allow-read --allow-env --allow-ffi --allow-net --allow-sys \ - * --config benches/deno/deno.json \ - * benches/deno/diagnostics/test262_compare.ts --manifest /tmp/t262.json \ + * --config benches/js/deno.json \ + * benches/js/diagnostics/test262_compare.ts --manifest /tmp/t262.json \ * 2>/dev/null > /tmp/t262-compare.json * * Background + bucket taxonomy: docs/conformance_test262.md §Differential. diff --git a/benches/deno/diagnostics/wasm_format_probe.ts b/benches/js/diagnostics/wasm_format_probe.ts similarity index 98% rename from benches/deno/diagnostics/wasm_format_probe.ts rename to benches/js/diagnostics/wasm_format_probe.ts index 6c4b285cc..9de2aac57 100644 --- a/benches/deno/diagnostics/wasm_format_probe.ts +++ b/benches/js/diagnostics/wasm_format_probe.ts @@ -28,7 +28,7 @@ * # ... make the source change, then rebuild: * deno task build:wasm:all:deno * deno run --allow-read --allow-env --allow-net --allow-sys \ - * benches/deno/diagnostics/wasm_format_probe.ts \ + * benches/js/diagnostics/wasm_format_probe.ts \ * --baseline crates/tsv_wasm/pkg/all/deno.baseline/tsv_wasm.js * * Add --control /tsv_wasm.js for a two-instance @@ -37,7 +37,7 @@ * A/A only (no source change — just capture the baseline number + floor): * * deno run --allow-read --allow-env --allow-net --allow-sys \ - * benches/deno/diagnostics/wasm_format_probe.ts + * benches/js/diagnostics/wasm_format_probe.ts * * Defaults to the zzz corpus the native profiling tools use, for comparability. * Human output goes to stderr; stdout stays clean for a future --json. diff --git a/benches/deno/diagnostics/wasm_json_probe.ts b/benches/js/diagnostics/wasm_json_probe.ts similarity index 98% rename from benches/deno/diagnostics/wasm_json_probe.ts rename to benches/js/diagnostics/wasm_json_probe.ts index 074534539..a5e6a5e98 100644 --- a/benches/deno/diagnostics/wasm_json_probe.ts +++ b/benches/js/diagnostics/wasm_json_probe.ts @@ -11,7 +11,7 @@ * JSON.parse cost. * * Run: deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys \ - * benches/deno/diagnostics/wasm_json_probe.ts 2>&1 >/dev/null + * benches/js/diagnostics/wasm_json_probe.ts 2>&1 >/dev/null */ import { DevReposLoader, group_by_language } from '../lib/corpus.ts'; diff --git a/benches/deno/divergence_audit.ts b/benches/js/divergence_audit.ts similarity index 100% rename from benches/deno/divergence_audit.ts rename to benches/js/divergence_audit.ts diff --git a/benches/js/install_deps.ts b/benches/js/install_deps.ts new file mode 100644 index 000000000..5b35d4990 --- /dev/null +++ b/benches/js/install_deps.ts @@ -0,0 +1,43 @@ +/** + * Install the bench harness's npm deps. `package.json` is the single source of + * truth for versions; both runtimes consume the resulting `node_modules` (Deno + * via `nodeModulesDir: "manual"` in deno.json, Node directly). Runs the one + * installer — `npm install` — then force-fetches the pure-wasm + * `@oxc-parser/binding-wasm32-wasi` binding (the oxc-parser WASM bench row), + * which npm skips on a non-wasm32 host because its metadata declares + * `cpu: wasm32`. Portable (node: builtins only), so `deno run` or `node` both + * drive it. + * + * Usage: deno task bench:install (or: node benches/js/install_deps.ts) + */ +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); + +function npm(args: Array): void { + const r = spawnSync('npm', args, { cwd: here, stdio: 'inherit' }); + if (r.status !== 0) process.exit(r.status ?? 1); +} + +// 1. Main install — everything in `dependencies` (the wasi binding in +// `optionalDependencies` is skipped here on a non-wasm host, no error). +npm(['install']); + +// 2. The oxc-parser WASM binding: pure-wasm, runtime-portable, but npm's cpu +// gate skips it. `--force` bypasses the gate; `--no-save` keeps it out of +// package.json/lock (where an optionalDependency entry would make a later +// forced reinstall no-op as "up to date"). oxc ships every binding at the +// oxc-parser version, so that pin is the single source of truth. +const pkg = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8')) as { + dependencies?: Record; +}; +const oxc_version = pkg.dependencies?.['oxc-parser']; +const wasi = '@oxc-parser/binding-wasm32-wasi'; +if (oxc_version) { + npm(['install', `${wasi}@${oxc_version}`, '--force', '--no-save']); +} else { + console.error(`warning: oxc-parser not pinned in dependencies; ${wasi} (oxc-parser-wasm row) will be unavailable`); +} diff --git a/benches/deno/lib/binary_sizes.ts b/benches/js/lib/binary_sizes.ts similarity index 72% rename from benches/deno/lib/binary_sizes.ts rename to benches/js/lib/binary_sizes.ts index 672172877..ca769341d 100644 --- a/benches/deno/lib/binary_sizes.ts +++ b/benches/js/lib/binary_sizes.ts @@ -1,14 +1,27 @@ /** * Binary/WASM size collection and reporting. * - * Collects file sizes for each implementation's compiled binary: - * - tsv: native (.so/.dylib/.dll) and WASM (.wasm) - * - biome: WASM (.wasm) from npm cache - * - oxc-parser: native (.node) and WASM (.wasm via binding-wasm32-wasi) from npm cache - * - oxfmt: native (.node) from npm cache (no WASM variant) + * Collects file sizes for each implementation's compiled binary. Native-kind + * rows are labeled by their binding mechanism: tsv ships an FFI shared library + * AND an N-API addon; oxc-parser and oxfmt ship N-API `.node` bindings. + * - tsv: FFI (.so/.dylib/.dll), N-API (.node), and WASM (.wasm) + * - biome: WASM (.wasm) from node_modules + * - oxc-parser: N-API (.node) and WASM (.wasm via binding-wasm32-wasi) from node_modules + * - oxfmt: N-API (.node) from node_modules (no WASM variant) + * + * Portable across runtimes: uses `node:` builtins (Deno supports them) and the + * shared `runtime.ts` platform normalizer instead of `Deno.*`. The alternative + * impls' bindings now live in the harness `node_modules` (flat, no version dir), + * not the Deno npm cache — see benches/js/package.json. */ -import type { AllVersions } from './versions.ts'; +import { execFile } from 'node:child_process'; +import { readdir, stat } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; +import { current_arch, current_os, current_runtime } from './runtime.ts'; + +const exec_file = promisify(execFile); /** Binary kind for grouping comparisons */ export type BinaryKind = 'wasm' | 'native'; @@ -35,41 +48,44 @@ export interface BinarySize { * consumer must reference the same constant. Renaming a label here updates both. */ const LABELS = { - tsv_native: 'tsv (native)', - tsv_format_native: 'tsv format (native)', - tsv_parse_native: 'tsv parse (native)', + tsv_ffi: 'tsv (ffi)', + tsv_napi: 'tsv (napi)', + tsv_format_ffi: 'tsv format (ffi)', + tsv_parse_ffi: 'tsv parse (ffi)', tsv_format_wasm: 'tsv_format_wasm', tsv_parse_wasm: 'tsv_parse_wasm', tsv_wasm: 'tsv_wasm', biome_wasm: 'biome (wasm)', - oxc_parser_native: 'oxc-parser (native)', - oxfmt_native: 'oxfmt (native)', + oxc_parser_napi: 'oxc-parser (napi)', + oxfmt_napi: 'oxfmt (napi)', oxc_parser_wasm: 'oxc-parser (wasm)', - oxc_combined_native: 'oxc-parser+oxfmt (native)', + oxc_combined_napi: 'oxc-parser+oxfmt (napi)', } as const; -/** Get the Deno npm cache base path */ -function get_deno_npm_cache_path(): string { - const deno_dir = Deno.env.get('DENO_DIR') ?? `${Deno.env.get('HOME')}/.cache/deno`; - return `${deno_dir}/npm/registry.npmjs.org`; +/** Absolute path to the bench harness's `node_modules` (where the alternative + * impls and their native/wasm bindings install — the single dep tree both + * runtimes consume; see package.json). Bindings sit at `//...` + * with no version subdirectory (unlike the old Deno npm cache). */ +function node_modules_dir(): string { + return fileURLToPath(new URL('../node_modules', import.meta.url)); } -/** Get platform string for npm native binding packages (e.g., "linux-x64") */ +/** Get platform string for npm native binding packages (e.g., "linux-x64"). + * Translates the shared normalizer's Rust-style names to npm's. */ function get_npm_platform(): { os: string; arch: string } { - const os = Deno.build.os === 'windows' ? 'win32' : Deno.build.os; - const arch = Deno.build.arch === 'x86_64' + const os = current_os() === 'windows' ? 'win32' : current_os(); + const arch = current_arch() === 'x86_64' ? 'x64' - : Deno.build.arch === 'aarch64' + : current_arch() === 'aarch64' ? 'arm64' - : Deno.build.arch; + : current_arch(); return { os, arch }; } /** Try to stat a file and return its size, or null if it doesn't exist */ async function file_size(path: string): Promise { try { - const stat = await Deno.stat(path); - return stat.size; + return (await stat(path)).size; } catch { return null; } @@ -83,13 +99,15 @@ async function file_size(path: string): Promise { */ async function gzip_size(path: string): Promise { try { - const output = await new Deno.Command('gzip', { - args: ['-c', path], - stdout: 'piped', - stderr: 'null', - }).output(); - if (!output.success) return null; - return output.stdout.length; + // `encoding: 'buffer'` so `stdout` is the raw gzip bytes (a string would + // corrupt the binary and miscount). `maxBuffer` lifted well above the + // default 1 MB — biome's wasm gzips to ~9 MB. Works under both runtimes + // (Deno's node:child_process needs the bench's `--allow-env`/`--allow-run`). + const { stdout } = await exec_file('gzip', ['-c', path], { + encoding: 'buffer', + maxBuffer: 256 * 1024 * 1024, + }); + return (stdout as unknown as Buffer).length; } catch { // Subprocess failed to spawn — gzip not on PATH (likely Windows without WSL). return null; @@ -117,8 +135,8 @@ async function resolve_first( ): Promise<{ path: string; bytes: number } | null> { for (const dir of dirs) { try { - for await (const e of Deno.readDir(dir)) { - if (e.isFile && e.name.endsWith(ext)) { + for (const e of await readdir(dir, { withFileTypes: true })) { + if (e.isFile() && e.name.endsWith(ext)) { const path = `${dir}/${e.name}`; const bytes = await file_size(path); if (bytes !== null) return { path, bytes }; @@ -148,16 +166,15 @@ async function push_resolved( * ships per-libc variants (`-gnu`, `-musl`) plus a libc-agnostic fallback. */ function napi_binding_dirs( - npm_cache: string, + node_modules: string, scope_pkg: string, os: string, arch: string, - version: string, ): string[] { return [ - `${npm_cache}/${scope_pkg}-${os}-${arch}-gnu/${version}`, - `${npm_cache}/${scope_pkg}-${os}-${arch}-musl/${version}`, - `${npm_cache}/${scope_pkg}-${os}-${arch}/${version}`, + `${node_modules}/${scope_pkg}-${os}-${arch}-gnu`, + `${node_modules}/${scope_pkg}-${os}-${arch}-musl`, + `${node_modules}/${scope_pkg}-${os}-${arch}`, ]; } @@ -165,61 +182,74 @@ function napi_binding_dirs( * Collect binary sizes for all implementations. * * Uses known paths for tsv binaries (relative to project root) - * and Deno npm cache paths for npm packages. Computes gzipped size + * and node_modules paths for npm packages. Computes gzipped size * alongside raw size; gzip is shelled out and parallelized across all * collected entries, so adding it costs roughly the slowest single * compression (biome's 35 MB dominates). */ export async function collect_binary_sizes( - versions: AllVersions, options?: { has_native?: boolean; + has_napi?: boolean; has_wasm?: boolean; has_oxc?: boolean; has_biome?: boolean; }, ): Promise { - const project_root = new URL('../../..', import.meta.url).pathname; - const npm_cache = get_deno_npm_cache_path(); + const project_root = fileURLToPath(new URL('../../..', import.meta.url)); + const node_modules = node_modules_dir(); + const os = current_os(); // Stage 1: collect (label, kind, path) for everything that exists. const staged: StagedEntry[] = []; // tsv native (FFI shared library) + const ext = os === 'darwin' ? 'dylib' : os === 'windows' ? 'dll' : 'so'; + const prefix = os === 'windows' ? '' : 'lib'; if (options?.has_native !== false) { - const ext = Deno.build.os === 'darwin' ? 'dylib' : Deno.build.os === 'windows' ? 'dll' : 'so'; - const prefix = Deno.build.os === 'windows' ? '' : 'lib'; await push_size( staged, - LABELS.tsv_native, + LABELS.tsv_ffi, 'native', `${project_root}/target/release/${prefix}tsv_ffi.${ext}`, ); // tsv format-only native — the native mirror of @fuzdev/tsv_format_wasm: // dropping the convert/JSON layer (and the parse exports) leaves a - // scope-matched comparison against oxfmt (native), which is format-only + // scope-matched comparison against oxfmt (napi), which is format-only // too. Built into a separate target dir (deno task build:ffi:format) so it // doesn't clobber the full libtsv_ffi the perf rows load; omitted from the // table when that build hasn't been run. await push_size( staged, - LABELS.tsv_format_native, + LABELS.tsv_format_ffi, 'native', `${project_root}/target/ffi-format/release/${prefix}tsv_ffi.${ext}`, ); // tsv parse-only native — the native mirror of @fuzdev/tsv_parse_wasm: // keeps the parse exports + the convert/JSON layer and drops the printers, - // so it's scope-matched to oxc-parser (native), which also materializes a + // so it's scope-matched to oxc-parser (napi), which also materializes a // JSON AST. Separate target dir (deno task build:ffi:parse); omitted when // unbuilt. await push_size( staged, - LABELS.tsv_parse_native, + LABELS.tsv_parse_ffi, 'native', `${project_root}/target/ffi-parse/release/${prefix}tsv_ffi.${ext}`, ); } + // tsv N-API addon — the Node/Bun native path (the sibling of the FFI library + // Deno loads). Same engine, different binding boundary; sized from the built + // cdylib (the shipped `.node` is a byte-identical copy). Omitted when unbuilt. + if (options?.has_napi !== false) { + await push_size( + staged, + LABELS.tsv_napi, + 'native', + `${project_root}/target/release/${prefix}tsv_napi.${ext}`, + ); + } + // tsv WASM — three builds from one crate via the `format`/`parse` features: // pkg/format/deno (format-only, @fuzdev/tsv_format_wasm), pkg/parse/deno // (parse-only, @fuzdev/tsv_parse_wasm), and pkg/all/deno (both, @@ -251,31 +281,29 @@ export async function collect_binary_sizes( staged, LABELS.biome_wasm, 'wasm', - [`${npm_cache}/@biomejs/wasm-bundler/${versions.biome.wasm}`], + [`${node_modules}/@biomejs/wasm-bundler`], '.wasm', ); } // oxc-parser + oxfmt if (options?.has_oxc !== false) { - const { os, arch } = get_npm_platform(); - const oxc_ver = versions.oxc['oxc-parser']; - const oxfmt_ver = versions.oxc.oxfmt; + const { os: npm_os, arch: npm_arch } = get_npm_platform(); await push_resolved( staged, - LABELS.oxc_parser_native, + LABELS.oxc_parser_napi, 'native', - napi_binding_dirs(npm_cache, '@oxc-parser/binding', os, arch, oxc_ver), + napi_binding_dirs(node_modules, '@oxc-parser/binding', npm_os, npm_arch), '.node', ); // oxfmt native binding (0.50.0+: @oxfmt/binding-{platform}; pre-0.49: @oxfmt/{platform}). await push_resolved( staged, - LABELS.oxfmt_native, + LABELS.oxfmt_napi, 'native', - napi_binding_dirs(npm_cache, '@oxfmt/binding', os, arch, oxfmt_ver), + napi_binding_dirs(node_modules, '@oxfmt/binding', npm_os, npm_arch), '.node', ); @@ -284,7 +312,7 @@ export async function collect_binary_sizes( staged, LABELS.oxc_parser_wasm, 'wasm', - [`${npm_cache}/@oxc-parser/binding-wasm32-wasi/${oxc_ver}`], + [`${node_modules}/@oxc-parser/binding-wasm32-wasi`], '.wasm', ); } @@ -317,9 +345,16 @@ function build_display_entries(sizes: BinarySize[]): { wasm_entries: DisplayRow[]; native_entries: DisplayRow[]; } { - const tsv_native = sizes.find((s) => s.label === LABELS.tsv_native); - // "vs tsv" wasm anchor: the flagship full build (`tsv_wasm`), the artifact - // the bench executes. + // Native "vs tsv" anchor: the binding THIS runtime actually benchmarks — FFI + // under Deno, N-API under Node/Bun — so the native ratios compare against the + // row that produced the run's native timing, and a Node-only build (FFI lib + // absent) still gets a populated anchor. Falls back to the FFI lib when the + // runtime's own native binding isn't on disk. + const native_anchor_label = current_runtime() === 'deno' ? LABELS.tsv_ffi : LABELS.tsv_napi; + const tsv_native = sizes.find((s) => s.label === native_anchor_label) ?? + sizes.find((s) => s.label === LABELS.tsv_ffi); + // "vs tsv" wasm anchor: the flagship full build (`tsv_wasm`), the artifact the + // bench executes — identical `.wasm` across runtimes (only the JS glue differs). const tsv_wasm = sizes.find((s) => s.label === LABELS.tsv_wasm); const wasm_sizes = sizes.filter((s) => s.kind === 'wasm'); @@ -329,11 +364,11 @@ function build_display_entries(sizes: BinarySize[]): { // the sum of the parts' gzipped sizes; that overstates wire size slightly // (two streams don't share a dictionary) but matches how npm ships them // — each binding is its own tarball. - const oxc_parser = native_sizes.find((s) => s.label === LABELS.oxc_parser_native); - const oxfmt_entry = native_sizes.find((s) => s.label === LABELS.oxfmt_native); + const oxc_parser = native_sizes.find((s) => s.label === LABELS.oxc_parser_napi); + const oxfmt_entry = native_sizes.find((s) => s.label === LABELS.oxfmt_napi); const combined_oxc: BinarySize | null = oxc_parser && oxfmt_entry ? { - label: LABELS.oxc_combined_native, + label: LABELS.oxc_combined_napi, bytes: oxc_parser.bytes + oxfmt_entry.bytes, gzip_bytes: oxc_parser.gzip_bytes !== null && oxfmt_entry.gzip_bytes !== null ? oxc_parser.gzip_bytes + oxfmt_entry.gzip_bytes diff --git a/benches/deno/lib/biome.ts b/benches/js/lib/biome.ts similarity index 80% rename from benches/deno/lib/biome.ts rename to benches/js/lib/biome.ts index ac4beeda2..7f731dcca 100644 --- a/benches/deno/lib/biome.ts +++ b/benches/js/lib/biome.ts @@ -6,10 +6,10 @@ import { type Language, LANGUAGE_EXTENSIONS, type TsvImplementation } from './types.ts'; import type { BiomeVersions } from './versions.ts'; - -// Pre-import WASM package to ensure it's in the module graph -import '@biomejs/wasm-bundler'; -import { Biome } from '@biomejs/js-api/bundler'; +// Type-only — `import type` is erased, so referencing `Biome` here does NOT load +// the WASM package at this module's import. The value import is deferred to +// `init()` (see there) so a load-time crash can't escape the registry's skip. +import type { Biome } from '@biomejs/js-api/bundler'; /** * Biome implementation using WASM. @@ -36,8 +36,15 @@ export class BiomeImplementation implements TsvImplementation { this.versions = versions; } - // deno-lint-ignore require-await async init(): Promise { + // Load the WASM package + js-api lazily (not as static top-level imports) + // so a load-time failure — e.g. Bun's wasm-bindgen-`start` incompatibility + // with `@biomejs/wasm-bundler` — throws HERE, inside init_implementations' + // per-impl try/catch (and is skipped), instead of throwing during this + // module's static import graph and aborting the whole registry. The + // wasm-bundler import runs first so it's registered before js-api loads. + await import('@biomejs/wasm-bundler'); + const { Biome } = await import('@biomejs/js-api/bundler'); this._biome = new Biome(); const { projectKey } = this._biome.openProject('/tmp'); this._project_key = projectKey; diff --git a/benches/deno/lib/canonical.ts b/benches/js/lib/canonical.ts similarity index 100% rename from benches/deno/lib/canonical.ts rename to benches/js/lib/canonical.ts diff --git a/benches/deno/lib/canonical_test.ts b/benches/js/lib/canonical_test.ts similarity index 100% rename from benches/deno/lib/canonical_test.ts rename to benches/js/lib/canonical_test.ts diff --git a/benches/deno/lib/check_artifact_freshness.ts b/benches/js/lib/check_artifact_freshness.ts similarity index 82% rename from benches/deno/lib/check_artifact_freshness.ts rename to benches/js/lib/check_artifact_freshness.ts index b7ce27c86..d6c328de6 100644 --- a/benches/deno/lib/check_artifact_freshness.ts +++ b/benches/js/lib/check_artifact_freshness.ts @@ -3,8 +3,8 @@ * * `deno task bench` and `deno task corpus:compare:format` build the Rust + WASM * artifacts before running, so what they measure is fresh by construction. The - * `:run` variants (`bench:run`, `corpus:compare:format:run`) deliberately SKIP that - * build — that's the path for iterating on the measurement/reporting harness + * `:run` variants (`bench:deno:run` / `bench:node:run`, `corpus:compare:format:run`) + * deliberately SKIP that build — that's the path for iterating on the measurement/reporting harness * (this directory's `.ts`) without paying the cargo + wasm-pack cost on every * tweak. `deno task smoke` likewise skips the build (it's the fast pre-bench * sanity check) and relies on this guard rather than rebuilding. @@ -33,13 +33,16 @@ * is newer than its sources, so the check passes silently. */ -import { walk } from '@std/fs'; +import { readdir, stat } from 'node:fs/promises'; +import { env, exit } from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { current_runtime } from './runtime.ts'; /** Crates whose source compiles into every tsv artifact (the shared core). */ const CORE_CRATES = ['tsv_lang', 'tsv_html', 'tsv_ts', 'tsv_css', 'tsv_svelte']; /** Absolute path to the workspace `crates/` directory. */ -const CRATES_DIR = new URL('../../../crates', import.meta.url).pathname; +const CRATES_DIR = fileURLToPath(new URL('../../../crates', import.meta.url)); export interface ArtifactCheck { /** Human-readable label used in messages, e.g. `FFI (release)`. */ @@ -96,16 +99,19 @@ async function newest_source_mtime(crates: string[]): Promise { for (const crate of crates) { const root = `${CRATES_DIR}/${crate}`; try { - const st = await Deno.stat(`${root}/Cargo.toml`); - consider(st.mtime?.getTime() ?? 0, `${crate}/Cargo.toml`); + const st = await stat(`${root}/Cargo.toml`); + consider(st.mtimeMs, `${crate}/Cargo.toml`); } catch { // crate may not have a Cargo.toml at this path — ignore } try { - for await (const entry of walk(`${root}/src`, { includeDirs: false })) { - if (!entry.isFile || !entry.name.endsWith('.rs')) continue; - const st = await Deno.stat(entry.path); - consider(st.mtime?.getTime() ?? 0, entry.path.slice(CRATES_DIR.length + 1)); + const src = `${root}/src`; + for (const relative of await readdir(src, { recursive: true })) { + if (!relative.endsWith('.rs')) continue; + const full = `${src}/${relative}`; + const st = await stat(full); + if (!st.isFile()) continue; + consider(st.mtimeMs, full.slice(CRATES_DIR.length + 1)); } } catch { // crate may not have a src/ directory — ignore @@ -123,7 +129,7 @@ async function newest_source_mtime(crates: string[]): Promise { * staleness; returns normally when everything is fresh (or only warns). */ export async function check_artifact_freshness(checks: readonly ArtifactCheck[]): Promise { - const stale_ok = Deno.env.get('BENCH_STALE_OK') === '1'; + const stale_ok = env.BENCH_STALE_OK === '1'; const core = await newest_source_mtime(CORE_CRATES); const stale: StaleArtifact[] = []; @@ -133,8 +139,8 @@ export async function check_artifact_freshness(checks: readonly ArtifactCheck[]) let artifact_ms: number; try { - const st = await Deno.stat(check.path); - artifact_ms = st.mtime?.getTime() ?? 0; + const st = await stat(check.path); + artifact_ms = st.mtimeMs; } catch { stale.push({ label: check.label, @@ -196,11 +202,14 @@ export async function check_artifact_freshness(checks: readonly ArtifactCheck[]) lines.push(''); console.error(lines.join('\n')); - if (fatal) Deno.exit(1); + if (fatal) exit(1); } -/** Path to a deno-target WASM bundle's compiled `.wasm` file for the given variant. */ +/** Path to the executed WASM bundle's compiled `.wasm` for the given variant — + * the runtime's own wasm-pack target (Deno → `deno`, Node/Bun → `nodejs`). */ export function wasm_artifact_path(variant: 'format' | 'parse' | 'all'): string { - return new URL(`../../../crates/tsv_wasm/pkg/${variant}/deno/tsv_wasm_bg.wasm`, import.meta.url) - .pathname; + const target = current_runtime() === 'deno' ? 'deno' : 'nodejs'; + return fileURLToPath( + new URL(`../../../crates/tsv_wasm/pkg/${variant}/${target}/tsv_wasm_bg.wasm`, import.meta.url), + ); } diff --git a/benches/deno/lib/compare_cli.ts b/benches/js/lib/compare_cli.ts similarity index 100% rename from benches/deno/lib/compare_cli.ts rename to benches/js/lib/compare_cli.ts diff --git a/benches/deno/lib/corpus.ts b/benches/js/lib/corpus.ts similarity index 85% rename from benches/deno/lib/corpus.ts rename to benches/js/lib/corpus.ts index d57be54b3..4827d1db0 100644 --- a/benches/deno/lib/corpus.ts +++ b/benches/js/lib/corpus.ts @@ -7,8 +7,8 @@ * Both support `load()` (collect all) and `stream()` (async generator for GC). */ -import { exists } from '@std/fs/exists'; -import { walk } from '@std/fs/walk'; +import { fs_exists } from '@fuzdev/fuz_util/fs.ts'; +import { readdir, readFile } from 'node:fs/promises'; import { basename, dirname, extname, join, resolve } from 'node:path'; import type { Language, Logger, SourceFile } from './types.ts'; @@ -106,14 +106,14 @@ async function has_companion_options(file_path: string): Promise { if (options_dir_cache.has(dir)) { if (options_dir_cache.get(dir)) return true; } else { - const dir_has_options = await exists(join(dir, 'options.json')); + const dir_has_options = await fs_exists(join(dir, 'options.json')); options_dir_cache.set(dir, dir_has_options); if (dir_has_options) return true; } // Check name.options.json (per-file, not cached) const name_without_ext = basename(file_path).replace(/\.[^.]+$/, ''); - return exists(join(dir, `${name_without_ext}.options.json`)); + return fs_exists(join(dir, `${name_without_ext}.options.json`)); } // @@ -126,31 +126,45 @@ interface WalkOptions { skip?: (path: string) => boolean | Promise; } -/** Walk a directory and yield source files one at a time */ +/** Walk a directory and yield source files one at a time. + * + * Uses `node:fs/promises` recursive `readdir` (identical output under Deno and + * Node) for the directory traversal, then reads each file's content lazily so + * the per-file content (the memory-heavy part) is yielded and released one at a + * time. Paths are sorted for deterministic, runtime-independent ordering. The + * `extensions` set replaces `@std/walk`'s `exts` filter; directories fall out + * naturally (no matching extension), and `should_exclude` does the post-hoc + * pruning exactly as before. */ async function* walk_corpus( dir_path: string, options: WalkOptions = {}, ): AsyncGenerator { const extensions = options.extensions ?? DEFAULT_EXTENSIONS; + const ext_set = new Set(extensions.map((e) => `.${e.toLowerCase()}`)); + + const relative_paths = await readdir(dir_path, { recursive: true }); + relative_paths.sort(); - for await (const entry of walk(dir_path, { exts: extensions, includeDirs: false })) { - if (should_exclude(entry.path)) continue; + for (const relative of relative_paths) { + const path = join(dir_path, relative); + if (!ext_set.has(extname(path).toLowerCase())) continue; + if (should_exclude(path)) continue; - const language = detect_language(entry.path); + const language = detect_language(path); if (!language) continue; - if (options.skip && (await options.skip(entry.path))) continue; + if (options.skip && (await options.skip(path))) continue; try { - const content = await Deno.readTextFile(entry.path); + const content = await readFile(path, 'utf8'); yield { - path: entry.path, + path, content, language, bytes: new TextEncoder().encode(content).length, }; } catch (e) { - console.warn(`Warning: Could not read ${entry.path}: ${e}`); + console.warn(`Warning: Could not read ${path}: ${e}`); } } } @@ -246,7 +260,7 @@ export class DevReposLoader { const skip = is_object ? entry.skip : undefined; const resolved_path = resolve(entry_path); - if (!(await exists(resolved_path))) { + if (!(await fs_exists(resolved_path))) { continue; } @@ -290,7 +304,7 @@ export class DirectoryLoader { async *stream(logger: Logger = console.log): AsyncGenerator { const resolved_path = resolve(this.#path); - if (!(await exists(resolved_path))) { + if (!(await fs_exists(resolved_path))) { throw new Error(`Directory not found: ${this.#path}`); } diff --git a/benches/deno/lib/diff.ts b/benches/js/lib/diff.ts similarity index 100% rename from benches/deno/lib/diff.ts rename to benches/js/lib/diff.ts diff --git a/benches/deno/lib/divergence/expected_errors.ts b/benches/js/lib/divergence/expected_errors.ts similarity index 100% rename from benches/deno/lib/divergence/expected_errors.ts rename to benches/js/lib/divergence/expected_errors.ts diff --git a/benches/deno/lib/divergence/expected_errors_test.ts b/benches/js/lib/divergence/expected_errors_test.ts similarity index 100% rename from benches/deno/lib/divergence/expected_errors_test.ts rename to benches/js/lib/divergence/expected_errors_test.ts diff --git a/benches/deno/lib/divergence/fixture_coverage_test.ts b/benches/js/lib/divergence/fixture_coverage_test.ts similarity index 100% rename from benches/deno/lib/divergence/fixture_coverage_test.ts rename to benches/js/lib/divergence/fixture_coverage_test.ts diff --git a/benches/deno/lib/divergence/mod.ts b/benches/js/lib/divergence/mod.ts similarity index 100% rename from benches/deno/lib/divergence/mod.ts rename to benches/js/lib/divergence/mod.ts diff --git a/benches/deno/lib/divergence/patterns.ts b/benches/js/lib/divergence/patterns.ts similarity index 100% rename from benches/deno/lib/divergence/patterns.ts rename to benches/js/lib/divergence/patterns.ts diff --git a/benches/deno/lib/divergence/patterns_test.ts b/benches/js/lib/divergence/patterns_test.ts similarity index 100% rename from benches/deno/lib/divergence/patterns_test.ts rename to benches/js/lib/divergence/patterns_test.ts diff --git a/benches/deno/lib/divergence/safety.ts b/benches/js/lib/divergence/safety.ts similarity index 100% rename from benches/deno/lib/divergence/safety.ts rename to benches/js/lib/divergence/safety.ts diff --git a/benches/deno/lib/divergence/safety_test.ts b/benches/js/lib/divergence/safety_test.ts similarity index 100% rename from benches/deno/lib/divergence/safety_test.ts rename to benches/js/lib/divergence/safety_test.ts diff --git a/benches/deno/lib/divergence/validation.ts b/benches/js/lib/divergence/validation.ts similarity index 100% rename from benches/deno/lib/divergence/validation.ts rename to benches/js/lib/divergence/validation.ts diff --git a/benches/deno/lib/ffi.ts b/benches/js/lib/ffi.ts similarity index 100% rename from benches/deno/lib/ffi.ts rename to benches/js/lib/ffi.ts diff --git a/benches/deno/lib/implementations.ts b/benches/js/lib/implementations.ts similarity index 93% rename from benches/deno/lib/implementations.ts rename to benches/js/lib/implementations.ts index 2dd149f54..e1f37d052 100644 --- a/benches/deno/lib/implementations.ts +++ b/benches/js/lib/implementations.ts @@ -12,7 +12,9 @@ import type { Language, Logger, TsvImplementation } from './types.ts'; import { CanonicalImplementation } from './canonical.ts'; import { NativeImplementation } from './ffi.ts'; +import { NapiImplementation } from './napi.ts'; import { WasmImplementation } from './wasm.ts'; +import { current_runtime } from './runtime.ts'; import { OxcImplementation } from './oxc.ts'; import { OxcWasmImplementation } from './oxc_wasm.ts'; import { BiomeImplementation } from './biome.ts'; @@ -26,8 +28,8 @@ export interface InitializedImplementations { versions: AllVersions; /** Canonical implementation (prettier + svelte/compiler) - always available */ canonical: CanonicalImplementation; - /** Native FFI implementation - undefined if not built */ - native: NativeImplementation | undefined; + /** Native implementation — FFI under Deno, N-API under Node/Bun; undefined if not built */ + native: NativeImplementation | NapiImplementation | undefined; /** WASM implementation - undefined if not built */ wasm: WasmImplementation | undefined; /** OXC implementation (oxc-parser + oxfmt) - undefined if not available */ @@ -64,11 +66,17 @@ export async function init_implementations( ): Promise { const { logger = console.log, skip_missing = true, require_canonical = true } = options; - // Load all versions once from deno.json + // Load all versions once from package.json const versions = await load_all_versions(); + // The native path is runtime-specific: Deno loads the C-FFI library via + // Deno.dlopen; Node/Bun load the N-API addon via process.dlopen. Same engine, + // different binding boundary — one is instantiated per runtime. + const is_deno = current_runtime() === 'deno'; + const native_label = is_deno ? 'Native (FFI)' : 'Native (N-API)'; + const canonical = new CanonicalImplementation(versions.canonical); - const native = new NativeImplementation(); + const native = is_deno ? new NativeImplementation() : new NapiImplementation(); const wasm = new WasmImplementation(); logger('Initializing implementations...'); @@ -86,14 +94,14 @@ export async function init_implementations( } // Initialize native (optional) - let native_impl: NativeImplementation | undefined; + let native_impl: NativeImplementation | NapiImplementation | undefined; try { await native.init(); - logger(' ✓ Native (FFI)'); + logger(` ✓ ${native_label}`); native_impl = native; } catch (e) { if (skip_missing) { - logger(` ⚠ Native (FFI): not available`); + logger(` ⚠ ${native_label}: not available`); } else { throw e; } @@ -193,7 +201,7 @@ export interface BenchmarkTaskOptions { * engine as `tsv`, routed through the awaited async path to measure the * per-file await tax the async-only impls (`prettier`, `oxfmt`) pay. That tax * sits below the run-to-run noise floor, so the row is kept OUT of the - * published `report.{json,md}` and the regression baseline (where a + * published `report..{json,md}` and the regression baseline (where a * noise-level delta would throw spurious flags) — it's an on-demand * re-confirmation tool, not a standing measurement. */ diff --git a/benches/js/lib/napi.ts b/benches/js/lib/napi.ts new file mode 100644 index 000000000..948169924 --- /dev/null +++ b/benches/js/lib/napi.ts @@ -0,0 +1,133 @@ +/** + * N-API bindings to native tsv (the Node/Bun native path). + * + * The runtime sibling of `ffi.ts` (Deno's `Deno.dlopen` C-FFI path): same engine + * (`tsv_napi`, built from the same language crates), different binding boundary. + * Loaded with `process.dlopen`, which accepts the built cdylib directly + * (`target/release/libtsv_napi.so`) as an N-API addon — no `.node` rename, so + * `build:napi` is just `cargo build -p tsv_napi --release`. + * + * Unlike FFI there are no raw pointers and no manual free: napi-rs marshals the + * JS string in and the returned `String` out. `parse_` returns a JSON + * string (parity with FFI/WASM — the host `JSON.parse`s it), and engine errors + * surface as thrown JS errors (napi-rs converts the `napi::Error`), so there is + * no `{"error": …}` envelope to inspect — a throw just propagates. + * + * Only instantiated under Node/Bun (see `implementations.ts`); importing this + * module under Deno is harmless because `process.dlopen` is only touched in + * `init()`. + */ + +import { stat } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { current_os } from './runtime.ts'; +import type { Language, TsvImplementation } from './types.ts'; + +/** The N-API addon's exported functions (snake_case `js_name`s, matching WASM/FFI). */ +interface NapiAddon { + parse_svelte: (source: string) => string; + parse_internal_svelte: (source: string) => void; + format_svelte: (source: string) => string; + parse_typescript: (source: string) => string; + parse_internal_typescript: (source: string) => void; + format_typescript: (source: string) => string; + parse_css: (source: string) => string; + parse_internal_css: (source: string) => void; + format_css: (source: string) => string; +} + +/** Path to the built `tsv_napi` cdylib (loaded directly as an N-API addon). */ +export function get_napi_library_path(): string { + const os = current_os(); + const ext = os === 'darwin' ? 'dylib' : os === 'windows' ? 'dll' : 'so'; + const prefix = os === 'windows' ? '' : 'lib'; + const project_root = fileURLToPath(new URL('../../../', import.meta.url)); + return `${project_root}target/release/${prefix}tsv_napi.${ext}`; +} + +export class NapiImplementation implements TsvImplementation { + // Distinct from FFI's `'native'` so the two native bindings are + // self-describing (one is instantiated per runtime — FFI under Deno, N-API + // under Node/Bun). Nothing branches on this tag; rows key on `tracking_key`. + name = 'napi' as const; + private _addon: NapiAddon | null = null; + + /** Languages supported for parsing */ + static readonly PARSE_LANGUAGES: Language[] = ['svelte', 'typescript', 'css']; + + /** Languages supported for formatting */ + static readonly FORMAT_LANGUAGES: Language[] = ['svelte', 'typescript', 'css']; + + private get addon(): NapiAddon { + if (!this._addon) throw new Error('N-API addon not initialized'); + return this._addon; + } + + async init(): Promise { + const path = get_napi_library_path(); + try { + await stat(path); + } catch { + throw new Error( + `N-API addon not found at ${path}. Run 'deno task build:napi' first.`, + ); + } + // `process.dlopen` loads a native addon from any path/extension into the + // passed module's `exports` — the supported way to load a `.so`/`.dylib` + // that isn't named `.node`. + const mod: { exports: NapiAddon } = { exports: {} as NapiAddon }; + process.dlopen(mod, path); + this._addon = mod.exports; + } + + supports_parse_language(language: Language): boolean { + return NapiImplementation.PARSE_LANGUAGES.includes(language); + } + + supports_format_language(language: Language): boolean { + return NapiImplementation.FORMAT_LANGUAGES.includes(language); + } + + private get parse_fns(): Record string> { + return { + svelte: this.addon.parse_svelte, + typescript: this.addon.parse_typescript, + css: this.addon.parse_css, + }; + } + + private get parse_internal_fns(): Record void> { + return { + svelte: this.addon.parse_internal_svelte, + typescript: this.addon.parse_internal_typescript, + css: this.addon.parse_internal_css, + }; + } + + private get format_fns(): Record string> { + return { + svelte: this.addon.format_svelte, + typescript: this.addon.format_typescript, + css: this.addon.format_css, + }; + } + + parse(source: string, language: Language): unknown { + // `parse_` returns a JSON string (the engine throws on parse error); + // materialize it the same way ffi.ts / wasm.ts do for an apples-to-apples + // `tsv-json`-style row. + return JSON.parse(this.parse_fns[language](source)); + } + + parse_internal(source: string, language: Language): void { + this.parse_internal_fns[language](source); + } + + format(source: string, language: Language): string { + return this.format_fns[language](source); + } + + dispose(): void { + this._addon = null; + } +} diff --git a/benches/deno/lib/oxc.ts b/benches/js/lib/oxc.ts similarity index 97% rename from benches/deno/lib/oxc.ts rename to benches/js/lib/oxc.ts index 534b71bc0..3e8827c40 100644 --- a/benches/deno/lib/oxc.ts +++ b/benches/js/lib/oxc.ts @@ -86,7 +86,7 @@ export class OxcImplementation implements TsvImplementation { // lazy variant: oxc's `experimentalLazy` raw transfer is setup-dominated // (~1.7ms/call on Node, ~2.1ms on Deno, vs ~0.7ms eager + ~0.16ms parse-only) — // it eagerly copies the whole AST transfer buffer, so it measures buffer setup, - // not parse speed, in any runtime. See `benches/deno/CLAUDE.md` → Fairness Caveats. + // not parse speed, in any runtime. See `benches/js/CLAUDE.md` → Fairness Caveats. return result.program; } diff --git a/benches/deno/lib/oxc_wasm.ts b/benches/js/lib/oxc_wasm.ts similarity index 77% rename from benches/deno/lib/oxc_wasm.ts rename to benches/js/lib/oxc_wasm.ts index 7f2c3d94e..4974af010 100644 --- a/benches/deno/lib/oxc_wasm.ts +++ b/benches/js/lib/oxc_wasm.ts @@ -1,14 +1,16 @@ /** * OXC WASM implementation wrapper (oxc-parser via wasm32-wasi) * - * Uses the browser entry point of @oxc-parser/binding-wasm32-wasi which - * works in Deno (the default CJS entry uses node:wasi which Deno doesn't support). + * Loads @oxc-parser/binding-wasm32-wasi via the entry each runtime can handle + * (see `init`): Deno gets the fetch-based browser entry, Node/Bun get the + * default `node:wasi` entry — so the oxc-parser-wasm row runs under all three. * * Supports: Parse only (TypeScript, JS). No formatting (oxfmt has no WASM variant). */ import { type Language, LANGUAGE_EXTENSIONS, type TsvImplementation } from './types.ts'; import type { OxcVersions } from './versions.ts'; +import { current_runtime } from './runtime.ts'; /** oxc-parser WASM module types (same API as native) */ interface OxcParserWasmModule { @@ -32,7 +34,15 @@ export class OxcWasmImplementation implements TsvImplementation { } async init(): Promise { - const mod = await import('@oxc-parser/binding-wasm32-wasi/parser.wasi-browser.js'); + // The WASI binding ships two entries: the default CJS entry uses `node:wasi` + // (Node/Bun support it, Deno doesn't), and an explicit fetch-based browser + // entry (`parser.wasi-browser.js`, `@napi-rs/wasm-runtime` + `WebAssembly`) + // that Deno can load but Node can't. Pick per runtime so the + // oxc-parser-wasm comparison row is available on all three. + const entry = current_runtime() === 'deno' + ? '@oxc-parser/binding-wasm32-wasi/parser.wasi-browser.js' + : '@oxc-parser/binding-wasm32-wasi'; + const mod = await import(entry); this._parser = mod as OxcParserWasmModule; } diff --git a/benches/deno/lib/report.ts b/benches/js/lib/report.ts similarity index 100% rename from benches/deno/lib/report.ts rename to benches/js/lib/report.ts diff --git a/benches/js/lib/runtime.ts b/benches/js/lib/runtime.ts new file mode 100644 index 000000000..75ce51789 --- /dev/null +++ b/benches/js/lib/runtime.ts @@ -0,0 +1,58 @@ +/** + * Tiny cross-runtime helpers for the bench harness. + * + * The harness runs under both Deno and Node from one shared codebase: shared + * modules use `node:` builtins directly (Deno supports them) and avoid `Deno.*` + * / `@std/*`. This module is the handful of spots where the two runtimes' own + * identifiers differ — platform naming and the runtime label itself — plus the + * `runtime` tag stamped onto every report row so a reader never has to guess + * what produced a number. The genuinely runtime-specific code (the native FFI + * vs N-API loader) lives in `ffi.ts` / `napi.ts`, not here. + */ + +import { arch as node_arch, platform as node_platform } from 'node:process'; + +/** The JS runtime executing the harness. Stamped on every report row. */ +export type Runtime = 'deno' | 'node' | 'bun'; + +/** Rust-target-style OS name (`linux` | `darwin` | `windows`), matching the + * old `Deno.build.os` values used for binary/library path construction. */ +export type Os = 'linux' | 'darwin' | 'windows'; + +/** Detect the current runtime. `typeof` on the undeclared globals is safe in + * every runtime (no ReferenceError). Order matters: Bun also defines a partial + * `process`, and Deno defines neither `Bun` nor a Node-only marker, so probe the + * distinctive globals first. */ +export function current_runtime(): Runtime { + if (typeof (globalThis as { Deno?: unknown }).Deno !== 'undefined') return 'deno'; + if (typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') return 'bun'; + return 'node'; +} + +/** Normalize `process.platform` to the `Deno.build.os` vocabulary the binary + * path / library-name logic was written against. */ +export function current_os(): Os { + switch (node_platform) { + case 'win32': + return 'windows'; + case 'darwin': + return 'darwin'; + default: + // linux (and the rare *bsd, treated as linux for our .so naming) + return 'linux'; + } +} + +/** Normalize `process.arch` to the Rust-target vocabulary (`x86_64` / + * `aarch64`) that matched `Deno.build.arch`, used to locate per-platform npm + * native bindings. Falls through verbatim for anything unmapped. */ +export function current_arch(): string { + switch (node_arch) { + case 'x64': + return 'x86_64'; + case 'arm64': + return 'aarch64'; + default: + return node_arch; + } +} diff --git a/benches/deno/lib/types.ts b/benches/js/lib/types.ts similarity index 90% rename from benches/deno/lib/types.ts rename to benches/js/lib/types.ts index fcde6a874..4ad1dbef5 100644 --- a/benches/deno/lib/types.ts +++ b/benches/js/lib/types.ts @@ -25,11 +25,6 @@ export const LANGUAGE_PRETTIER_PARSERS: Record = { css: 'css', }; -/** Extract version from npm specifier (e.g., "npm:prettier@3.7.4" -> "3.7.4") */ -export function extract_version(specifier: string): string { - const match = specifier.match(/@(\d+\.\d+\.\d+)/); - return match ? match[1] : 'unknown'; -} /** A source file loaded into memory for benchmarking */ export interface SourceFile { @@ -47,6 +42,7 @@ export interface SourceFile { export type ImplementationName = | 'canonical' | 'native' + | 'napi' | 'wasm' | 'oxc' | 'oxc-wasm' diff --git a/benches/js/lib/versions.ts b/benches/js/lib/versions.ts new file mode 100644 index 000000000..9d043baba --- /dev/null +++ b/benches/js/lib/versions.ts @@ -0,0 +1,96 @@ +/** + * Centralized version loading from package.json + * + * Single source of truth for all package versions used in benchmarks. + */ + +import { readFile } from 'node:fs/promises'; + +/** Canonical implementation versions */ +export interface CanonicalVersions { + prettier: string; + 'prettier-plugin-svelte': string; + svelte: string; + acorn: string; + '@sveltejs/acorn-typescript': string; +} + +/** OXC implementation versions */ +export interface OxcVersions { + 'oxc-parser': string; + oxfmt: string; +} + +/** Biome implementation versions */ +export interface BiomeVersions { + js_api: string; + wasm: string; +} + +/** All implementation versions */ +export interface AllVersions { + canonical: CanonicalVersions; + oxc: OxcVersions; + biome: BiomeVersions; +} + +/** Default versions when loading fails */ +const DEFAULT_VERSIONS: AllVersions = { + canonical: { + prettier: 'unknown', + 'prettier-plugin-svelte': 'unknown', + svelte: 'unknown', + acorn: 'unknown', + '@sveltejs/acorn-typescript': 'unknown', + }, + oxc: { + 'oxc-parser': 'unknown', + oxfmt: 'unknown', + }, + biome: { + js_api: 'unknown', + wasm: 'unknown', + }, +}; + +/** Strip a leading semver range marker (`^`/`~`/`>=`/etc.) from a package.json + * version spec, leaving the bare `x.y.z`. `'^4.4.3' -> '4.4.3'`. */ +function clean_version(spec: string | undefined): string { + if (!spec) return 'unknown'; + const m = spec.match(/(\d+\.\d+\.\d+)/); + return m ? m[1] : 'unknown'; +} + +/** + * Load all package versions from `package.json` — the single source of truth for + * the npm deps the bench measures against (both runtimes resolve from it; see + * benches/js/package.json). Reads `benches/js/package.json` `dependencies`. + */ +export async function load_all_versions(): Promise { + try { + const pkg_json_path = new URL('../package.json', import.meta.url).pathname; + const content = await readFile(pkg_json_path, 'utf8'); + const config = JSON.parse(content) as { dependencies?: Record }; + const deps = config.dependencies ?? {}; + + return { + canonical: { + prettier: clean_version(deps['prettier']), + 'prettier-plugin-svelte': clean_version(deps['prettier-plugin-svelte']), + svelte: clean_version(deps['svelte']), + acorn: clean_version(deps['acorn']), + '@sveltejs/acorn-typescript': clean_version(deps['@sveltejs/acorn-typescript']), + }, + oxc: { + 'oxc-parser': clean_version(deps['oxc-parser']), + oxfmt: clean_version(deps['oxfmt']), + }, + biome: { + js_api: clean_version(deps['@biomejs/js-api']), + wasm: clean_version(deps['@biomejs/wasm-bundler']), + }, + }; + } catch { + return DEFAULT_VERSIONS; + } +} diff --git a/benches/deno/lib/wasm.ts b/benches/js/lib/wasm.ts similarity index 68% rename from benches/deno/lib/wasm.ts rename to benches/js/lib/wasm.ts index 22409108c..e4c2d11bc 100644 --- a/benches/deno/lib/wasm.ts +++ b/benches/js/lib/wasm.ts @@ -2,8 +2,19 @@ * WASM bindings to tsv * * Uses wasm-pack generated bindings for WebAssembly performance testing. + * Runtime-aware: each runtime loads its own wasm-pack *target* bundle (same + * `tsv_wasm_bg.wasm`, different JS glue), both carrying the full export set + * including the benchmark-only `parse_internal_*`: + * - Deno: the `deno` target (ESM; explicit `default()` init) + * - Node/Bun: the `nodejs` target (CommonJS; self-initializing on require) + * The shipped `@fuzdev/tsv_wasm` (web) bundle is deliberately NOT used here — it + * curates out `parse_internal_*`, which the `tsv_wasm-internal` row needs. */ +import { stat } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { current_runtime } from './runtime.ts'; import type { Language, TsvImplementation } from './types.ts'; /** WASM module function signatures */ @@ -71,26 +82,33 @@ export class WasmImplementation implements TsvImplementation { } async init(): Promise { - const wasm_path = new URL( - '../../../crates/tsv_wasm/pkg/all/deno/tsv_wasm.js', - import.meta.url, - ).pathname; + const target = current_runtime() === 'deno' ? 'deno' : 'nodejs'; + const wasm_path = fileURLToPath( + new URL(`../../../crates/tsv_wasm/pkg/all/${target}/tsv_wasm.js`, import.meta.url), + ); try { - await Deno.stat(wasm_path); + await stat(wasm_path); } catch { throw new Error( `WASM module not found at ${wasm_path}. ` + - `Run 'deno task build:wasm:all:deno' first.`, + `Run 'deno task build:wasm:all:${target}' first.`, ); } - // Dynamic import of wasm-pack generated module - const module = await import(wasm_path); - - // wasm-pack for Deno generates a default export that initializes the module - if (typeof module.default === 'function') { - await module.default(); + // The deno target is ESM with an explicit `default()` initializer; the + // nodejs target is CommonJS and self-initializes on require. Load each in + // its native module system (both resolve to `any`), then read the same + // function names off both into the typed `WasmModule` shape below. + let module: WasmModule; + if (target === 'deno') { + const esm = await import(wasm_path); + if (typeof esm.default === 'function') { + await esm.default(); + } + module = esm; + } else { + module = createRequire(import.meta.url)(wasm_path); } this._module = { diff --git a/benches/js/package-lock.json b/benches/js/package-lock.json new file mode 100644 index 000000000..de33a11f3 --- /dev/null +++ b/benches/js/package-lock.json @@ -0,0 +1,1188 @@ +{ + "name": "@fuzdev/tsv-benches", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@fuzdev/tsv-benches", + "version": "0.0.0", + "dependencies": { + "@biomejs/js-api": "4.0.0", + "@biomejs/wasm-bundler": "2.4.16", + "@fuzdev/fuz_util": "0.64.0", + "@sveltejs/acorn-typescript": "1.0.10", + "acorn": "8.16.0", + "oxc-parser": "0.134.0", + "oxfmt": "0.53.0", + "prettier": "3.8.3", + "prettier-plugin-svelte": "4.1.1", + "svelte": "5.56.1", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^24" + } + }, + "node_modules/@biomejs/js-api": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@biomejs/js-api/-/js-api-4.0.0.tgz", + "integrity": "sha512-EOArR/6drRzM1/hwOIz1pZw90FL31Ud4Y7hEHGWVtMNmAwS9SrwZ8hMENGlLVXCeGW/kL46p8kX7eO6x9Nmezg==", + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "@biomejs/wasm-bundler": "^2.3.0", + "@biomejs/wasm-nodejs": "^2.3.0", + "@biomejs/wasm-web": "^2.3.0" + }, + "peerDependenciesMeta": { + "@biomejs/wasm-bundler": { + "optional": true + }, + "@biomejs/wasm-nodejs": { + "optional": true + }, + "@biomejs/wasm-web": { + "optional": true + } + } + }, + "node_modules/@biomejs/wasm-bundler": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/wasm-bundler/-/wasm-bundler-2.4.16.tgz", + "integrity": "sha512-BV/+7zBokC08DhC9RZMH61TX//GEVuTxUMiDMC6Cg3Lvr0va/3Jp8qDOx4JtHULsA/fFE6CdehP+VETVJotRrg==", + "license": "MIT OR Apache-2.0" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fuzdev/fuz_util": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_util/-/fuz_util-0.64.0.tgz", + "integrity": "sha512-NM/+olAhneQhIP2faPwPSftf20+qk4v/enFrQQTtrQLLsTeURv4oqhW7vB3S9QXW99T51c3CGyg+8yzoGnzxYg==", + "license": "MIT", + "engines": { + "node": ">=24.14" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + }, + "peerDependencies": { + "@fuzdev/blake3_wasm": "^0.1.0", + "@types/estree": "^1", + "@types/node": "^24", + "esm-env": "^1.2.2", + "svelte": "^5", + "svelte-docinfo": ">=0.2.0", + "zod": "^4.0.14" + }, + "peerDependenciesMeta": { + "@fuzdev/blake3_wasm": { + "optional": true + }, + "@types/estree": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "svelte": { + "optional": true + }, + "svelte-docinfo": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.134.0.tgz", + "integrity": "sha512-N9Us7l/X9ZC3LA6eWSzPyduvBPXV1eRyDPwM6/UWpxwwXGsatb8131+d2L8UsmyHrixnKLHBd6UeH8wangV7fw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.134.0.tgz", + "integrity": "sha512-Ic2oPZESeCaD4+9cKRqp1GMYsTO9Q3Yi9HdY2x9x75ozbnC20sybFHzeBklmaVD9PBzd8KbkmNN0gy+SVlm7zw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.134.0.tgz", + "integrity": "sha512-1z9+nVJ1Awq4CPyHAthx5zOUrg5T1zc3dWt6juxwDcuejFGbdYzWJITkS1rv4DCdSTphDU3IW71MzyLV3BjRGQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.134.0.tgz", + "integrity": "sha512-MpofofaRZnxmYrY3lE8RTLHmE1KkX2q0elEeJ8sMcLbS8At76BjYSL6axssqhx29prGGDzIZ5lYFD+IXqaTzFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.134.0.tgz", + "integrity": "sha512-OqnPQY27vqWAbMnHfLcF8CVOUv2cCvdlTiqyK5qz5WCbH3XOLpYVQATv8S5UrR8bbEJCAqJLsI7W6cRFXAxCoA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.134.0.tgz", + "integrity": "sha512-0eqWl+PWrcwGC3b8DCB58w3QINAuZfpX2ULTGpI01GUMBc6zDKSpttWxvqPIydxuQEkGQTQRAXLLvc+vTDZbQw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.134.0.tgz", + "integrity": "sha512-YToasuDmyzpyTC1ztrvkaSXz8tP+YUbx041M/4SGxaRGiyMzsKkQ869KPUTGA57A6aVsb1/DiPX8XZQQeSFkiw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.134.0.tgz", + "integrity": "sha512-qMS7NLc2o8G7LLz53wisol+O7/YbMhtaGVhlfsTVLnrraf9kLFgzpiGjtQALLbdxX8uhx0Zmd4l+vY3s1/K4Gg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.134.0.tgz", + "integrity": "sha512-jUEsnxPXhrCYxswQLYvUOxZEE5UWFMkK5kBnrcPMj7TONz1pD0yKgPmOQCAx2LPoysJ/v2Sjg4RBUVoO2VXoZQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.134.0.tgz", + "integrity": "sha512-FU5xMUsXnMuWKVCGo43c6SJsnqHcsioqm32PHdDY39cIRJa/AZbo7RMYW0W5gYcNZqb8EMhELkMDwbJOFbUhtg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.134.0.tgz", + "integrity": "sha512-tPc7OAhslHVmNebho1RSEGL//7i7Nm39gbQ+gYreBYwzvDVqNwdQH3S13ccM+R1TpimQ+3bIyFMfnilJIGRtjQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.134.0.tgz", + "integrity": "sha512-TtWEC4MUAHodX5a5kGsmK8g5K49V5ewWfwGrXdbw+gXWLXWXuhi+ectNELmOvYIwaqPSTAry1HNS/hfXssaPHQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.134.0.tgz", + "integrity": "sha512-mF4uls8TA8SPXSDLpclJ6z9S+vaeUnNp95iUEfqwv9oVJUAE7B2j4crOj8UvByRXpbO5N+aAlJadQEyFlnXU6g==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.134.0.tgz", + "integrity": "sha512-hieAQplyJeCvzqdSAxpOOGvVCBVXo/ioIBxioHfsUrQu93Et7Hy52cCG/GHEnjImVyeqEIViyyjuB0nKghxz/w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.134.0.tgz", + "integrity": "sha512-OQnJAK4sFuNSLgsh2s/K+14a3kwbmqf2yh1JiADU9XSfDuRooZYbAxmmBVPiyQ97+jBIIA1x2oPZeYNto3Ioow==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.134.0.tgz", + "integrity": "sha512-RYLooe6g31q/PqNFN0NjR80IK/ARGsfLasAXL42LXonL+5Cyy9Or76rjBKQLAjERikJwbRU/sYW9Q5Tnpt1A4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.134.0.tgz", + "integrity": "sha512-h8bmHyvc0PxA811prUjfVpJlQAurOOiRbohY4QNGjCEz+L72G9CmWl28OOz3mevrYlURgUsprNuH8hDJXh1VOw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.134.0.tgz", + "integrity": "sha512-EPTfanpBMLNnSAWCDYpbJp1stmsf5x6hMsAwymf2J8ylRupIvO6FtKmHBMdc/wt43y08iz6ILlzFGIXe32/Kbw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.134.0.tgz", + "integrity": "sha512-yAwetF+fTr9lTMtmqvkkWiiMXvH/yjMxGzUDG2rTL/LV1lL37eZ2enUGIlIo0gwhBIKWgabDEidtil+kiqNpgQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.134.0.tgz", + "integrity": "sha512-4VY39G5tuGlBYiH13XbWqfLcwKygQEv0iyf8vtZ5NyLAGjI8M0MiYyLhj91GkWRznr+5VC0I+5R55HUDV/Rklw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.134.0.tgz", + "integrity": "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.53.0.tgz", + "integrity": "sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.53.0.tgz", + "integrity": "sha512-btHDfXckwdf9zgyAVznfZkf+GVyB0I1m1hlvaOMRx2xoyz3hphfPX97s89J3wfCN8QBETLtk4lQUaeOkrMuQOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.53.0.tgz", + "integrity": "sha512-k2RjMcSTkHjoOlsVGbL35JVzXL+oQco3GHPl/5kjebVF4oHNfE24In8F5isqBh9LBJucycWHKDXdGrCchdWcHQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.53.0.tgz", + "integrity": "sha512-65jIBE2H1l5SSs16fmv6/7b6sAx/WpvnsgDhVWK9qSjNFDUro7MPQ6q5UhpY7kl46yltfR046iAnxy/Bzqbiew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.53.0.tgz", + "integrity": "sha512-oYe1gkz7U49PCYrS9147d2fJZj8mDI4Di6AvlsU5fu9p+Tq8S7qqOMSZjUiVTLX8bXuSA9Lk/tIxuegVjkNYRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.53.0.tgz", + "integrity": "sha512-ailB2vLzGi629tymdAb2VYJyEHref7oqGxP+tRBrtRBxQrb6NV55JMT7xtGZ8uTeG2+Y9zojqW4LhJYxQnz9Pg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.53.0.tgz", + "integrity": "sha512-abh4mWBvOvD966sobqF7r103y2yYx7Rb4WGHLOS4+5igGqLbbPxS9aK5+45D6iUY7dWMsk3Muz9a8gUtufvqJA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.53.0.tgz", + "integrity": "sha512-z73PvuhJ8qA+cDbaiqbtopHglA91U4+y5wn2sTJJrnpB957d5P33FEuyP3DQIFd7ofljmDmfVT4G0CVGHZaJWg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.53.0.tgz", + "integrity": "sha512-I6bhOTroqc3ThrwZ89l2k3ivKuELhdPLbAcJhRNyjWvlgwb0vjRgEnVL1XLx5Jud04/ypNRZBykAWrSk6l/D+g==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.53.0.tgz", + "integrity": "sha512-w0p3JzB/PkkQjXALMJMqP9YfP3yq4w6zGsu5kezQmUnxRkN3b/Theg2l/nDgBsOcczxS3gL6Gam5XNAVrO6QJQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.53.0.tgz", + "integrity": "sha512-mzBhF6k1Yq1K/dqDmVe/AAafnlJfEpx7yfUiksyeWXJk5iSzZqBSxcsa02zIytYgQFRZ7h6WPZfwHg/DoOE1Kw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.53.0.tgz", + "integrity": "sha512-AlFCpnRQhogQFzZXWbO6xB6/Udy745L+eQNmDPGg7G/OeWsYmJc4jZYfUN5pQg0reOPWSED2mOQqKZOJM1U8cA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.53.0.tgz", + "integrity": "sha512-XD4ulY4f1DWbuuZXAqxhVn+gdPmrhnmojWtFN78ctVoupmS845fGhsUrk1HZXKQI+iymbaiz9vAjPsghHNQ7Ag==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.53.0.tgz", + "integrity": "sha512-xg8KWX0QnxmYWRe60CgHYWXI0ZOtBbqTsXvWiWrcl2XUHJ3fht2QerOk2iWvylzX3zNT2GpvBRxGoR4d3sxPRQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.53.0.tgz", + "integrity": "sha512-MWExpYBGvl+pIvVB/gj/CcWlN2al8AizT7rUbtaYaWNoQkhWARM6W3qpgoCr72CYSN9PborzPmM5MIRe2BrNdA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.53.0.tgz", + "integrity": "sha512-u4sajgO4nxgmJIgc/y2AqPhkdbOkQH8WugXpA1+pW0ESQhvGZ1oGq61Q4xMbJHJU1hFgtO18QNrcFYDPYH0gwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.53.0.tgz", + "integrity": "sha512-Yq9sOZoIOJ5xPjO0qOyHJS4CiPuTkB2en9auxZz7Ar2p5RaC7BzLyVVmAA7zz9/L9YnjjY1DwNxN+ivKXimN/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.53.0.tgz", + "integrity": "sha512-es1fVNZEkBqEcQtBpn19SYFgZF7FawlkCjkT/iImfEAus4gun8fBwB1E9hpV5LcR9B0DBNvRIXhW8BQk3JaE+Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.53.0.tgz", + "integrity": "sha512-QFmJs2bEu9AO4O6qsmEaZNGi6dFq8N+rT8EHAAnZIq/B9SeJDUbc4DzVxQ48MfDsL7D3sCZzo37zuTuspcURgg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.12.tgz", + "integrity": "sha512-On0QbLyaiAkVC4eXtgnXK9Kh2opit+3rcUSOc45DqJ2s/X2eXAHsGOKRSJ6IDagQEW5vPyivANfXUiqgXC67Rw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/oxc-parser": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.134.0.tgz", + "integrity": "sha512-Hs8fRG6A94BzMrMkGOtrUS7JQjmslfF+IvIXslf3QURzK3ud0QmFJRiYZjTe4TzAQnTfvlk4AwZnqIbrUjiE4w==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.134.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.134.0", + "@oxc-parser/binding-android-arm64": "0.134.0", + "@oxc-parser/binding-darwin-arm64": "0.134.0", + "@oxc-parser/binding-darwin-x64": "0.134.0", + "@oxc-parser/binding-freebsd-x64": "0.134.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.134.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.134.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.134.0", + "@oxc-parser/binding-linux-arm64-musl": "0.134.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.134.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.134.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.134.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.134.0", + "@oxc-parser/binding-linux-x64-gnu": "0.134.0", + "@oxc-parser/binding-linux-x64-musl": "0.134.0", + "@oxc-parser/binding-openharmony-arm64": "0.134.0", + "@oxc-parser/binding-wasm32-wasi": "0.134.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.134.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.134.0", + "@oxc-parser/binding-win32-x64-msvc": "0.134.0" + } + }, + "node_modules/oxfmt": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.53.0.tgz", + "integrity": "sha512-9cB5glS3Ip6NMuZ+6NYTao9FCWkDhRtPYCtR3QBu/NxHoFbgzzTvi41N4jxz/GqGfuLKspui1qb/LlSu2IbMcw==", + "license": "MIT", + "dependencies": { + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.53.0", + "@oxfmt/binding-android-arm64": "0.53.0", + "@oxfmt/binding-darwin-arm64": "0.53.0", + "@oxfmt/binding-darwin-x64": "0.53.0", + "@oxfmt/binding-freebsd-x64": "0.53.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.53.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.53.0", + "@oxfmt/binding-linux-arm64-gnu": "0.53.0", + "@oxfmt/binding-linux-arm64-musl": "0.53.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.53.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.53.0", + "@oxfmt/binding-linux-riscv64-musl": "0.53.0", + "@oxfmt/binding-linux-s390x-gnu": "0.53.0", + "@oxfmt/binding-linux-x64-gnu": "0.53.0", + "@oxfmt/binding-linux-x64-musl": "0.53.0", + "@oxfmt/binding-openharmony-arm64": "0.53.0", + "@oxfmt/binding-win32-arm64-msvc": "0.53.0", + "@oxfmt/binding-win32-ia32-msvc": "0.53.0", + "@oxfmt/binding-win32-x64-msvc": "0.53.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-4.1.1.tgz", + "integrity": "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^5.0.0" + } + }, + "node_modules/svelte": { + "version": "5.56.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.1.tgz", + "integrity": "sha512-eArsJmvl3xZVuTYD852PzIEdg2wgDdIZ1NEsIPbzAukHwi284B18No4nK2rCO9AwsWUDza4Cjvmoa4HaojTl5g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.9", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/benches/js/package.json b/benches/js/package.json new file mode 100644 index 000000000..3f6320f2d --- /dev/null +++ b/benches/js/package.json @@ -0,0 +1,26 @@ +{ + "name": "@fuzdev/tsv-benches", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "tsv benchmark harness deps — the single source of truth for the npm package versions the bench measures against. Consumed by BOTH runtimes: Deno reads it via `nodeModulesDir: \"manual\"` in deno.json, Node reads it directly. Install with `npm install` (the one installer). The jsr-only `@std/*` packages, if any, stay in deno.json.", + "//canonical-sync": "⚠ The 5 canonical packages (prettier, svelte, acorn, @sveltejs/acorn-typescript, prettier-plugin-svelte) are ALSO pinned as literals in crates/tsv_debug/src/deno/sidecar.ts and MUST match. Bumping any re-baselines the whole fixture corpus — see benches/js/CLAUDE.md §'Canonical baseline is coupled'. The other deps (oxc, oxfmt, biome, etc.) are free to bump independently.", + "dependencies": { + "acorn": "8.16.0", + "svelte": "5.56.1", + "@sveltejs/acorn-typescript": "1.0.10", + "prettier": "3.8.3", + "prettier-plugin-svelte": "4.1.1", + "@biomejs/js-api": "4.0.0", + "@biomejs/wasm-bundler": "2.4.16", + "oxc-parser": "0.134.0", + "oxfmt": "0.53.0", + "zod": "^4.4.3", + "@fuzdev/fuz_util": "0.64.0" + }, + "//types": "@types/node is types-only and dev-only (never ships, no runtime cost). Required so the shared harness's `node:` builtins type-check under `deno check`/`deno test` in npm-resolution mode — the portable-node:-builtins architecture's one new dep.", + "devDependencies": { + "@types/node": "^24" + }, + "//oxc-wasi": "The oxc-parser WASM bench row needs @oxc-parser/binding-wasm32-wasi — pure-wasm (runtime-portable) but its metadata declares `cpu: wasm32`, so npm skips it on any non-wasm host. It is NOT listed as a dep here: declaring it errors on x64, and as an optionalDependency npm records it as known-skipped so a later `--force` reinstall no-ops. Instead `deno task bench:install` (install_deps.ts) force-fetches it at the oxc-parser version (oxc ships all bindings in lockstep), so the `oxc-parser` pin above is its single source of truth too." +} diff --git a/benches/js/results/report.bun.json b/benches/js/results/report.bun.json new file mode 100644 index 000000000..afae6d421 --- /dev/null +++ b/benches/js/results/report.bun.json @@ -0,0 +1,648 @@ +{ + "version": 5, + "runtime": "bun", + "timestamp": "2026-06-26T16:14:57.136Z", + "git_commit": "e4c23f8e", + "corpus": { + "svelte": 1259, + "typescript": 4521, + "css": 182 + }, + "versions": { + "tsv": "0.1.0", + "svelte": "5.56.1", + "acorn": "8.16.0", + "acorn_ts": "1.0.10", + "prettier": "3.8.3", + "prettier_svelte": "4.1.1", + "oxc_parser": "0.134.0", + "oxfmt": "0.53.0" + }, + "binary_sizes": [ + { + "label": "tsv (ffi)", + "bytes": 3717440, + "kind": "native", + "gzip_bytes": 1550897 + }, + { + "label": "tsv format (ffi)", + "bytes": 2935520, + "kind": "native", + "gzip_bytes": 1251205 + }, + { + "label": "tsv parse (ffi)", + "bytes": 2068624, + "kind": "native", + "gzip_bytes": 867280 + }, + { + "label": "tsv (napi)", + "bytes": 3841744, + "kind": "native", + "gzip_bytes": 1591977 + }, + { + "label": "tsv_format_wasm", + "bytes": 2213824, + "kind": "wasm", + "gzip_bytes": 735053 + }, + { + "label": "tsv_parse_wasm", + "bytes": 1565374, + "kind": "wasm", + "gzip_bytes": 492040 + }, + { + "label": "tsv_wasm", + "bytes": 2881851, + "kind": "wasm", + "gzip_bytes": 920193 + }, + { + "label": "oxc-parser (napi)", + "bytes": 2658008, + "kind": "native", + "gzip_bytes": 1032774 + }, + { + "label": "oxfmt (napi)", + "bytes": 8016464, + "kind": "native", + "gzip_bytes": 3224684 + }, + { + "label": "oxc-parser (wasm)", + "bytes": 1866798, + "kind": "wasm", + "gzip_bytes": 518682 + } + ], + "entries": [ + { + "name": "svelte/compiler", + "group": "parse/svelte", + "mean_ns": 728864461.2857143, + "p50_ns": 730887582, + "p75_ns": 732617118.5, + "p90_ns": 738540108.4, + "p95_ns": 741938603.2, + "p99_ns": 744657399.04, + "min_ns": 718632947, + "max_ns": 745337098, + "std_dev_ns": 9550904.701259, + "cv": 0.013103814506761983, + "ops_per_second": 1.3719971999128666, + "sample_size": 7, + "files_processed": 1212, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "bun" + }, + { + "name": "tsv-json", + "group": "parse/svelte", + "mean_ns": 472169724.90909094, + "p50_ns": 469334492, + "p75_ns": 478261112, + "p90_ns": 480428576, + "p95_ns": 483046641, + "p99_ns": 485141093, + "min_ns": 460571229, + "max_ns": 485664706, + "std_dev_ns": 7785433.773089493, + "cv": 0.01648863398556199, + "ops_per_second": 2.117882505475197, + "sample_size": 11, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "bun" + }, + { + "name": "tsv_wasm-json", + "group": "parse/svelte", + "mean_ns": 481062947.8181818, + "p50_ns": 481432853, + "p75_ns": 483079967, + "p90_ns": 485456800, + "p95_ns": 486219557.5, + "p99_ns": 486829763.5, + "min_ns": 475621103, + "max_ns": 486982315, + "std_dev_ns": 3564148.756642849, + "cv": 0.0074089030818268765, + "ops_per_second": 2.0787300384189034, + "sample_size": 11, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "bun" + }, + { + "name": "tsv-internal", + "group": "parse/svelte", + "mean_ns": 26032361.102150537, + "p50_ns": 25992177.5, + "p75_ns": 26182614.75, + "p90_ns": 26441277.7, + "p95_ns": 26671045.3, + "p99_ns": 26844386.47, + "min_ns": 25418789, + "max_ns": 26904822, + "std_dev_ns": 240602.51125315513, + "cv": 0.009242439066861242, + "ops_per_second": 38.41372651816012, + "sample_size": 186, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "bun" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/svelte", + "mean_ns": 36520741.62015504, + "p50_ns": 36468436, + "p75_ns": 36762123.5, + "p90_ns": 37191084.4, + "p95_ns": 37439007.7, + "p99_ns": 60309138.01999991, + "min_ns": 35959197, + "max_ns": 78745422, + "std_dev_ns": 337735.017423474, + "cv": 0.009247759011473225, + "ops_per_second": 27.381700251347603, + "sample_size": 129, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "bun" + }, + { + "name": "prettier", + "group": "format/svelte", + "mean_ns": 4584984210.428572, + "p50_ns": 4575569718, + "p75_ns": 4627459201, + "p90_ns": 4663376881.6, + "p95_ns": 4684997987.8, + "p99_ns": 4702294872.76, + "min_ns": 4452506933, + "max_ns": 4706619094, + "std_dev_ns": 79884221.24759524, + "cv": 0.017423009018416712, + "ops_per_second": 0.21810325927088134, + "sample_size": 7, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1211, + "runtime": "bun" + }, + { + "name": "tsv", + "group": "format/svelte", + "mean_ns": 259153275.44444445, + "p50_ns": 258880666.5, + "p75_ns": 260852254.5, + "p90_ns": 263747416.4, + "p95_ns": 265626878.8, + "p99_ns": 270947544.56, + "min_ns": 257099585, + "max_ns": 272277711, + "std_dev_ns": 1773014.5919956989, + "cv": 0.006841567365702796, + "ops_per_second": 3.8587202815978814, + "sample_size": 18, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1211, + "runtime": "bun" + }, + { + "name": "tsv_wasm", + "group": "format/svelte", + "mean_ns": 353004023.21428573, + "p50_ns": 353212850, + "p75_ns": 355819213.5, + "p90_ns": 359512908.8, + "p95_ns": 362258267.5, + "p99_ns": 366735223.9, + "min_ns": 347098749, + "max_ns": 367854463, + "std_dev_ns": 3872582.931140447, + "cv": 0.01097036485839045, + "ops_per_second": 2.832828903462568, + "sample_size": 14, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1211, + "runtime": "bun" + }, + { + "name": "oxfmt", + "group": "format/svelte", + "mean_ns": 5500206606.857142, + "p50_ns": 5539623825, + "p75_ns": 5569601849, + "p90_ns": 5637599212.8, + "p95_ns": 5670783321.9, + "p99_ns": 5697330609.18, + "min_ns": 5346386416, + "max_ns": 5703967431, + "std_dev_ns": 131410626.60796654, + "cv": 0.02389194370337581, + "ops_per_second": 0.18181135209599103, + "sample_size": 7, + "files_processed": 1217, + "files_total": 1259, + "files_iterated": 1211, + "runtime": "bun" + }, + { + "name": "acorn-typescript", + "group": "parse/typescript", + "mean_ns": 6289739417.428572, + "p50_ns": 6303952023, + "p75_ns": 6348897993, + "p90_ns": 6369334923, + "p95_ns": 6378202902, + "p99_ns": 6385297285.2, + "min_ns": 6202401432, + "max_ns": 6387070881, + "std_dev_ns": 75981207.62265737, + "cv": 0.012080183705562908, + "ops_per_second": 0.15898909853547305, + "sample_size": 7, + "files_processed": 4189, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "bun" + }, + { + "name": "tsv-json", + "group": "parse/typescript", + "mean_ns": 1961496271.4, + "p50_ns": 1959466398, + "p75_ns": 1966034550, + "p90_ns": 1976085162.6, + "p95_ns": 1979435366.8, + "p99_ns": 1982115530.16, + "min_ns": 1944179852, + "max_ns": 1982785571, + "std_dev_ns": 14316215.259302609, + "cv": 0.0072986196650195725, + "ops_per_second": 0.5098148870230883, + "sample_size": 5, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "bun" + }, + { + "name": "tsv_wasm-json", + "group": "parse/typescript", + "mean_ns": 2154068843.5, + "p50_ns": 2152687789, + "p75_ns": 2154301775, + "p90_ns": 2157428100.8, + "p95_ns": 2158470209.4, + "p99_ns": 2159303896.28, + "min_ns": 2149773492, + "max_ns": 2159512318, + "std_dev_ns": 4084239.127686045, + "cv": 0.0018960578442097714, + "ops_per_second": 0.46423771599387137, + "sample_size": 4, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "bun" + }, + { + "name": "tsv-internal", + "group": "parse/typescript", + "mean_ns": 189667783.92, + "p50_ns": 189724316, + "p75_ns": 190232677, + "p90_ns": 191072936, + "p95_ns": 194071455.2, + "p99_ns": 199583036.38, + "min_ns": 188592635, + "max_ns": 201100146, + "std_dev_ns": 733895.6789317835, + "cv": 0.0038693744597202316, + "ops_per_second": 5.272376675322944, + "sample_size": 25, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "bun" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/typescript", + "mean_ns": 255082055.0526316, + "p50_ns": 255096701, + "p75_ns": 255852997.5, + "p90_ns": 256564904.5, + "p95_ns": 258655035.2, + "p99_ns": 260632239.04, + "min_ns": 253159440, + "max_ns": 261126540, + "std_dev_ns": 1246354.9102525811, + "cv": 0.004886094045288361, + "ops_per_second": 3.920307133301353, + "sample_size": 19, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "bun" + }, + { + "name": "oxc-parser", + "group": "parse/typescript", + "mean_ns": 957611696.25, + "p50_ns": 957197593.5, + "p75_ns": 957789548.5, + "p90_ns": 958025799, + "p95_ns": 958109273.5, + "p99_ns": 958176053.1, + "min_ns": 956813543, + "max_ns": 958192748, + "std_dev_ns": 587837.2395294324, + "cv": 0.0006138576229085323, + "ops_per_second": 1.0442646052841589, + "sample_size": 4, + "files_processed": 4329, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "bun" + }, + { + "name": "prettier", + "group": "format/typescript", + "mean_ns": 15613018604.285715, + "p50_ns": 15529194356, + "p75_ns": 15919914727, + "p90_ns": 16046157005.2, + "p95_ns": 16106896089.1, + "p99_ns": 16155487356.22, + "min_ns": 15218735583, + "max_ns": 16167635173, + "std_dev_ns": 387472609.68615776, + "cv": 0.02481727713946347, + "ops_per_second": 0.06404911345750294, + "sample_size": 7, + "files_processed": 4351, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "bun" + }, + { + "name": "tsv", + "group": "format/typescript", + "mean_ns": 805519337.6, + "p50_ns": 806250725, + "p75_ns": 840391190, + "p90_ns": 880028882, + "p95_ns": 885497513, + "p99_ns": 889872417.8, + "min_ns": 802950248, + "max_ns": 890966144, + "std_dev_ns": 1854103.7657046868, + "cv": 0.002301749541145574, + "ops_per_second": 1.2414351255420437, + "sample_size": 5, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "bun" + }, + { + "name": "tsv_wasm", + "group": "format/typescript", + "mean_ns": 1044001052.25, + "p50_ns": 1044566307, + "p75_ns": 1044973671, + "p90_ns": 1118223961.2, + "p95_ns": 1142640724.6, + "p99_ns": 1162174135.32, + "min_ns": 1043113088, + "max_ns": 1167057488, + "std_dev_ns": 908545.1204650855, + "cv": 0.0008702530696755679, + "ops_per_second": 0.9578534407075834, + "sample_size": 4, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "bun" + }, + { + "name": "oxfmt", + "group": "format/typescript", + "mean_ns": 1363519212.6, + "p50_ns": 1355165454, + "p75_ns": 1381540185, + "p90_ns": 1401896377.2, + "p95_ns": 1408681774.6, + "p99_ns": 1414110092.52, + "min_ns": 1317040421, + "max_ns": 1415467172, + "std_dev_ns": 37022018.21669685, + "cv": 0.027151812658438558, + "ops_per_second": 0.7333963399702814, + "sample_size": 5, + "files_processed": 4332, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "bun" + }, + { + "name": "svelte/compiler", + "group": "parse/css", + "mean_ns": 7343951.225225225, + "p50_ns": 7336876, + "p75_ns": 7486279.5, + "p90_ns": 10606607.600000001, + "p95_ns": 11296488.9, + "p99_ns": 12864551.76, + "min_ns": 7042548, + "max_ns": 21158647, + "std_dev_ns": 138261.49522418046, + "cv": 0.018826581357086866, + "ops_per_second": 136.16648168429685, + "sample_size": 555, + "files_processed": 152, + "files_total": 182, + "files_iterated": 148, + "runtime": "bun" + }, + { + "name": "tsv-json", + "group": "parse/css", + "mean_ns": 11377410.69837587, + "p50_ns": 11390323, + "p75_ns": 11809637, + "p90_ns": 12101057.8, + "p95_ns": 12287839, + "p99_ns": 15696367.679999996, + "min_ns": 10543479, + "max_ns": 24164657, + "std_dev_ns": 549257.3405313861, + "cv": 0.048276128470056266, + "ops_per_second": 87.8934606924887, + "sample_size": 431, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "bun" + }, + { + "name": "tsv_wasm-json", + "group": "parse/css", + "mean_ns": 14050957.390934844, + "p50_ns": 13997810.5, + "p75_ns": 14275600, + "p90_ns": 14640017.5, + "p95_ns": 14794672.25, + "p99_ns": 15129656.15, + "min_ns": 13417572, + "max_ns": 18619444, + "std_dev_ns": 381535.8457510684, + "cv": 0.02715372590889936, + "ops_per_second": 71.16952761134718, + "sample_size": 353, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "bun" + }, + { + "name": "tsv-internal", + "group": "parse/css", + "mean_ns": 4533501.793445879, + "p50_ns": 4527328.5, + "p75_ns": 4565652, + "p90_ns": 4632201.3, + "p95_ns": 4716243.75, + "p99_ns": 4912880.559999999, + "min_ns": 4449276, + "max_ns": 5316840, + "std_dev_ns": 37477.07117170949, + "cv": 0.008266693800781199, + "ops_per_second": 220.58003846953548, + "sample_size": 1007, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "bun" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/css", + "mean_ns": 6623939.280227596, + "p50_ns": 6613686, + "p75_ns": 6662819, + "p90_ns": 6724017.8, + "p95_ns": 6786626.4, + "p99_ns": 7010564.04, + "min_ns": 6561707, + "max_ns": 7407298, + "std_dev_ns": 43538.634833730066, + "cv": 0.006572921790465763, + "ops_per_second": 150.96756743906028, + "sample_size": 703, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "bun" + }, + { + "name": "prettier", + "group": "format/css", + "mean_ns": 255228261.63157895, + "p50_ns": 252833780.5, + "p75_ns": 262525003.5, + "p90_ns": 269761706.6, + "p95_ns": 277200035.55, + "p99_ns": 286361722.31, + "min_ns": 241813472, + "max_ns": 288652144, + "std_dev_ns": 9230971.681002587, + "cv": 0.036167513824654184, + "ops_per_second": 3.918061399656031, + "sample_size": 19, + "files_processed": 181, + "files_total": 182, + "files_iterated": 154, + "runtime": "bun" + }, + { + "name": "tsv", + "group": "format/css", + "mean_ns": 9466830.891304348, + "p50_ns": 9457865, + "p75_ns": 9504906.75, + "p90_ns": 9571512.1, + "p95_ns": 9629489.25, + "p99_ns": 9891006.8, + "min_ns": 9358262, + "max_ns": 10598309, + "std_dev_ns": 55753.85322152691, + "cv": 0.005889389370284304, + "ops_per_second": 105.63197034802205, + "sample_size": 506, + "files_processed": 154, + "files_total": 182, + "files_iterated": 154, + "runtime": "bun" + }, + { + "name": "tsv_wasm", + "group": "format/css", + "mean_ns": 12821630.520775624, + "p50_ns": 12816470, + "p75_ns": 12878888, + "p90_ns": 13004824.6, + "p95_ns": 13164270.399999999, + "p99_ns": 13559164.36, + "min_ns": 12690601, + "max_ns": 14076666, + "std_dev_ns": 76525.9079946149, + "cv": 0.005968500486004146, + "ops_per_second": 77.99320050438536, + "sample_size": 361, + "files_processed": 154, + "files_total": 182, + "files_iterated": 154, + "runtime": "bun" + }, + { + "name": "oxfmt", + "group": "format/css", + "mean_ns": 351428089.26666665, + "p50_ns": 347780595, + "p75_ns": 359555229, + "p90_ns": 362666840.4, + "p95_ns": 363450223.8, + "p99_ns": 364183367.96, + "min_ns": 338852220, + "max_ns": 364366654, + "std_dev_ns": 8939247.9013189, + "cv": 0.025436919171636624, + "ops_per_second": 2.845532359370373, + "sample_size": 15, + "files_processed": 181, + "files_total": 182, + "files_iterated": 154, + "runtime": "bun" + } + ], + "suppressed_noise": { + "oxfmt::textToDoc": 1 + } +} \ No newline at end of file diff --git a/benches/js/results/report.bun.md b/benches/js/results/report.bun.md new file mode 100644 index 000000000..eba281fbf --- /dev/null +++ b/benches/js/results/report.bun.md @@ -0,0 +1,191 @@ +# tsv benchmark results + +**Runtime:** bun + +**Date:** 2026-06-26T16:14:57.136Z — tsv 0.1.0 (e4c23f8e) + +**Corpus:** 1259 Svelte (1.8 MB), 4521 TypeScript (16.2 MB), 182 CSS (0.2 MB) — 5962 files, 18.2 MB total + +**Versions:** svelte@5.56.1, acorn@8.16.0, acorn-typescript@1.0.10, prettier@3.8.3, prettier-plugin-svelte@4.1.1, oxc-parser@0.134.0, oxfmt@0.53.0 + +**Methodology:** Single-threaded — every implementation formats/parses one file at a time, measured sequentially with no cross-file parallelism. The numbers are per-file, single-core latency/throughput, not the multi-core batch throughput a CLI gets formatting many files at once. + +## parse/svelte + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs svelte/compiler (speedup) | +| ----------------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------------------------- | +| svelte/compiler | 1.37 | 7 | 730.89 | 732.62 | 738.54 | — | — | 718.63 | 745.34 | baseline | +| tsv-json | 2.12 | 11 | 469.33 | 478.26 | 480.43 | 483.05 | 485.14 | 460.57 | 485.66 | 1.54x | +| tsv_wasm-json | 2.08 | 11 | 481.43 | 483.08 | 485.46 | 486.22 | 486.83 | 475.62 | 486.98 | 1.52x | +| tsv-internal | 38.41 | 186 | 25.99 | 26.18 | 26.44 | 26.67 | 26.84 | 25.42 | 26.90 | 28.0x | +| tsv_wasm-internal | 27.38 | 129 | 36.47 | 36.76 | 37.19 | 37.44 | 60.31 | 35.96 | 78.75 | 20.0x | + +**Files (intersection):** 1210 + +**Throughput:** svelte/compiler 2.4 MB/s, tsv-json 3.7 MB/s, tsv_wasm-json 3.6 MB/s, tsv-internal 67.2 MB/s, tsv_wasm-internal 47.9 MB/s + +**Coverage:** svelte/compiler 1212/1259 (96%), tsv-json 1220/1259 (96%), tsv_wasm-json 1220/1259 (96%), tsv-internal 1220/1259 (96%), tsv_wasm-internal 1220/1259 (96%) + +**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 18.1x tsv-internal, tsv_wasm-json 13.2x tsv_wasm-internal + +## format/svelte + +| Task Name | ops/sec | n | p50 (s) | p75 (s) | p90 (s) | p95 (s) | p99 (s) | min (s) | max (s) | vs prettier (speedup) | +| --------- | ------- | -- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | --------------------- | +| prettier | 0.22 | 7 | 4.58 | 4.63 | 4.66 | — | — | 4.45 | 4.71 | baseline | +| tsv | 3.86 | 18 | 0.26 | 0.26 | 0.26 | 0.27 | 0.27 | 0.26 | 0.27 | 17.7x | +| tsv_wasm | 2.83 | 14 | 0.35 | 0.36 | 0.36 | 0.36 | 0.37 | 0.35 | 0.37 | 13.0x | +| oxfmt | 0.18 | 7 | 5.54 | 5.57 | 5.64 | — | — | 5.35 | 5.70 | 0.83x | + +**Files (intersection):** 1211 + +**Throughput:** prettier 0.4 MB/s, tsv 6.7 MB/s, tsv_wasm 5.0 MB/s, oxfmt 0.3 MB/s + +**Coverage:** prettier 1220/1259 (96%), tsv 1220/1259 (96%), tsv_wasm 1220/1259 (96%), oxfmt 1217/1259 (96%) + +## parse/typescript + +| Task Name | ops/sec | n | p50 (s) | p75 (s) | p90 (s) | p95 (s) | p99 (s) | min (s) | max (s) | vs acorn-typescript (speedup) | +| ----------------- | ------- | -- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ----------------------------- | +| acorn-typescript | 0.16 | 7 | 6.30 | 6.35 | 6.37 | — | — | 6.20 | 6.39 | baseline | +| tsv-json | 0.51 | 5 | 1.96 | 1.97 | 1.98 | — | — | 1.94 | 1.98 | 3.21x | +| tsv_wasm-json | 0.46 | 4 | 2.15 | 2.15 | 2.16 | — | — | 2.15 | 2.16 | 2.92x | +| tsv-internal | 5.27 | 25 | 0.19 | 0.19 | 0.19 | 0.19 | 0.20 | 0.19 | 0.20 | 33.2x | +| tsv_wasm-internal | 3.92 | 19 | 0.26 | 0.26 | 0.26 | 0.26 | 0.26 | 0.25 | 0.26 | 24.7x | +| oxc-parser | 1.04 | 4 | 0.96 | 0.96 | 0.96 | — | — | 0.96 | 0.96 | 6.57x | + +**Files (intersection):** 4120 + +**Throughput:** acorn-typescript 2.5 MB/s, tsv-json 8.2 MB/s, tsv_wasm-json 7.4 MB/s, tsv-internal 84.4 MB/s, tsv_wasm-internal 62.8 MB/s, oxc-parser 16.7 MB/s + +**Coverage:** acorn-typescript 4189/4521 (92%), tsv-json 4260/4521 (94%), tsv_wasm-json 4260/4521 (94%), tsv-internal 4260/4521 (94%), tsv_wasm-internal 4260/4521 (94%), oxc-parser 4329/4521 (95%) + +**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 10.3x tsv-internal, tsv_wasm-json 8.4x tsv_wasm-internal + +## format/typescript + +| Task Name | ops/sec | n | p50 (s) | p75 (s) | p90 (s) | p95 (s) | p99 (s) | min (s) | max (s) | vs prettier (speedup) | +| --------- | ------- | - | ------- | ------- | ------- | ------- | ------- | ------- | ------- | --------------------- | +| prettier | 0.06 | 7 | 15.53 | 15.92 | 16.05 | — | — | 15.22 | 16.17 | baseline | +| tsv | 1.24 | 5 | 0.81 | 0.84 | 0.88 | — | — | 0.80 | 0.89 | 19.4x | +| tsv_wasm | 0.96 | 4 | 1.04 | 1.04 | 1.12 | — | — | 1.04 | 1.17 | 15.0x | +| oxfmt | 0.73 | 5 | 1.36 | 1.38 | 1.40 | — | — | 1.32 | 1.42 | 11.5x | + +**Files (intersection):** 4219 + +**Throughput:** prettier 1.0 MB/s, tsv 19.9 MB/s, tsv_wasm 15.4 MB/s, oxfmt 11.8 MB/s + +**Coverage:** prettier 4351/4521 (96%), tsv 4260/4521 (94%), tsv_wasm 4260/4521 (94%), oxfmt 4332/4521 (95%) + +## parse/css + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs svelte/compiler (speedup) | +| ----------------- | ------- | ---- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------------------------- | +| svelte/compiler | 136.17 | 555 | 7.34 | 7.49 | 10.61 | 11.30 | 12.86 | 7.04 | 21.16 | baseline | +| tsv-json | 87.89 | 431 | 11.39 | 11.81 | 12.10 | 12.29 | 15.70 | 10.54 | 24.16 | 0.65x | +| tsv_wasm-json | 71.17 | 353 | 14.00 | 14.28 | 14.64 | 14.79 | 15.13 | 13.42 | 18.62 | 0.52x | +| tsv-internal | 220.58 | 1007 | 4.53 | 4.57 | 4.63 | 4.72 | 4.91 | 4.45 | 5.32 | 1.62x | +| tsv_wasm-internal | 150.97 | 703 | 6.61 | 6.66 | 6.72 | 6.79 | 7.01 | 6.56 | 7.41 | 1.11x | + +**Files (intersection):** 148 + +**Throughput:** svelte/compiler 24.9 MB/s, tsv-json 16.0 MB/s, tsv_wasm-json 13.0 MB/s, tsv-internal 40.3 MB/s, tsv_wasm-internal 27.6 MB/s + +**Coverage:** svelte/compiler 152/182 (83%), tsv-json 154/182 (84%), tsv_wasm-json 154/182 (84%), tsv-internal 154/182 (84%), tsv_wasm-internal 154/182 (84%) + +**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 2.5x tsv-internal, tsv_wasm-json 2.1x tsv_wasm-internal + +## format/css + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs prettier (speedup) | +| --------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | --------------------- | +| prettier | 3.92 | 19 | 252.83 | 262.53 | 269.76 | 277.20 | 286.36 | 241.81 | 288.65 | baseline | +| tsv | 105.63 | 506 | 9.46 | 9.50 | 9.57 | 9.63 | 9.89 | 9.36 | 10.60 | 27.0x | +| tsv_wasm | 77.99 | 361 | 12.82 | 12.88 | 13.00 | 13.16 | 13.56 | 12.69 | 14.08 | 19.9x | +| oxfmt | 2.85 | 15 | 347.78 | 359.56 | 362.67 | 363.45 | 364.18 | 338.85 | 364.37 | 0.73x | + +**Files (intersection):** 154 + +**Throughput:** prettier 0.8 MB/s, tsv 20.4 MB/s, tsv_wasm 15.0 MB/s, oxfmt 0.5 MB/s + +**Coverage:** prettier 181/182 (99%), tsv 154/182 (84%), tsv_wasm 154/182 (84%), oxfmt 181/182 (99%) + +_Note: every `Nx` is speedup form — values > 1 mean self is faster. File counts come from the per-group `Files (intersection):` / `Coverage:` lines and the Comparisons table row labels._ + +## Binary Sizes + +| Binary | Size | Gzipped | vs tsv | vs tsv (gz) | +| --- | ---: | ---: | ---: | ---: | +| tsv_format_wasm | 2.2 MB | 735.1 KB | 0.8x | 0.8x | +| tsv_parse_wasm | 1.6 MB | 492.0 KB | 0.5x | 0.5x | +| tsv_wasm | 2.9 MB | 920.2 KB | — | — | +| oxc-parser (wasm) | 1.9 MB | 518.7 KB | 0.6x | 0.6x | +| tsv (ffi) | 3.7 MB | 1.6 MB | 1.0x | 1.0x | +| tsv format (ffi) | 2.9 MB | 1.3 MB | 0.8x | 0.8x | +| tsv parse (ffi) | 2.1 MB | 867.3 KB | 0.5x | 0.5x | +| tsv (napi) | 3.8 MB | 1.6 MB | — | — | +| oxc-parser+oxfmt (napi) | 10.7 MB | 4.3 MB | 2.8x | 2.7x | +| oxc-parser (napi) | 2.7 MB | 1.0 MB | 0.7x | 0.6x | +| oxfmt (napi) | 8.0 MB | 3.2 MB | 2.1x | 2.0x | + +_Gzipped ≈ npm-tarball wire size (`gzip -c`, system default level). `vs tsv (gz)` compares gzipped bytes; `vs tsv` compares raw on-disk bytes._ + +## Comparisons to tsv (speedup) + +| Benchmark | Comparisons | +| --- | --- | +| format svelte (1211f) | **17.7x** prettier, **21.2x** oxfmt | +| format typescript (4219f) | **19.4x** prettier, **1.69x** oxfmt | +| format css (154f) | **27.0x** prettier, **37.1x** oxfmt | +| parse svelte (1210f) | **1.54x** svelte | +| parse typescript (4120f) | **3.21x** svelte, **0.49x** oxc-parser | +| parse css (148f) | **0.65x** svelte | + +## Comparisons to tsv_wasm (speedup) + +| Benchmark | Comparisons | +| --- | --- | +| format svelte (1211f) | **13.0x** prettier | +| format typescript (4219f) | **15.0x** prettier | +| format css (154f) | **19.9x** prettier | +| parse svelte (1210f) | **1.52x** svelte | +| parse typescript (4120f) | **2.92x** svelte | +| parse css (148f) | **0.52x** svelte | + +_`Nx` is speedup — self is N× faster than the named opponent. `(Mf)` is the self impl's iterated count (per-group intersection in default mode; per-impl success set in `BENCH_MODE=union`). Parse canonical: svelte/compiler for .svelte/.css, acorn-typescript for .ts. oxc-parser (native and wasm) serializes the AST to JSON in Rust and deserializes it in JS — the same eager materialization as tsv-json/tsv_wasm-json, so these parse rows are apples-to-apples. tsv-internal/tsv_wasm-internal are parse-only (no JS materialization) and have no oxc counterpart — oxc exposes no comparably cheap mode (its JS API always serializes; experimentalLazy is setup-dominated). Format groups include parse time — each formatter parses internally._ + +## Skipped Files + +1371 unique file+error combinations — Svelte 167, TypeScript 1144, CSS 60. + +**Per-benchmark skip counts:** +- parse/typescript: acorn-typescript: 332 +- parse/typescript: tsv-json: 261 +- parse/typescript: tsv_wasm-json: 261 +- parse/typescript: tsv-internal: 261 +- parse/typescript: tsv_wasm-internal: 261 +- format/typescript: tsv: 261 +- format/typescript: tsv_wasm: 261 +- parse/typescript: oxc-parser: 192 +- format/typescript: oxfmt: 189 +- format/typescript: prettier: 170 +- parse/svelte: svelte/compiler: 47 +- format/svelte: oxfmt: 42 +- parse/svelte: tsv-json: 39 +- parse/svelte: tsv_wasm-json: 39 +- parse/svelte: tsv-internal: 39 +- parse/svelte: tsv_wasm-internal: 39 +- format/svelte: prettier: 39 +- format/svelte: tsv: 39 +- format/svelte: tsv_wasm: 39 +- parse/css: svelte/compiler: 30 +- parse/css: tsv-json: 28 +- parse/css: tsv_wasm-json: 28 +- parse/css: tsv-internal: 28 +- parse/css: tsv_wasm-internal: 28 +- format/css: tsv: 28 +- format/css: tsv_wasm: 28 +- format/css: prettier: 1 +- format/css: oxfmt: 1 + +_Per-file detail omitted. Re-run with `--verbose` to include error messages and failure sets per file._ diff --git a/benches/js/results/report.deno.json b/benches/js/results/report.deno.json new file mode 100644 index 000000000..f26f9415e --- /dev/null +++ b/benches/js/results/report.deno.json @@ -0,0 +1,736 @@ +{ + "version": 5, + "runtime": "deno", + "timestamp": "2026-06-26T15:56:20.597Z", + "git_commit": "e4c23f8e", + "corpus": { + "svelte": 1258, + "typescript": 4521, + "css": 182 + }, + "versions": { + "tsv": "0.1.0", + "svelte": "5.56.1", + "acorn": "8.16.0", + "acorn_ts": "1.0.10", + "prettier": "3.8.3", + "prettier_svelte": "4.1.1", + "oxc_parser": "0.134.0", + "oxfmt": "0.53.0", + "biome": "2.4.16" + }, + "binary_sizes": [ + { + "label": "tsv (ffi)", + "bytes": 3717440, + "kind": "native", + "gzip_bytes": 1550897 + }, + { + "label": "tsv format (ffi)", + "bytes": 2935520, + "kind": "native", + "gzip_bytes": 1251205 + }, + { + "label": "tsv parse (ffi)", + "bytes": 2068624, + "kind": "native", + "gzip_bytes": 867280 + }, + { + "label": "tsv (napi)", + "bytes": 3841744, + "kind": "native", + "gzip_bytes": 1591977 + }, + { + "label": "tsv_format_wasm", + "bytes": 2213824, + "kind": "wasm", + "gzip_bytes": 735053 + }, + { + "label": "tsv_parse_wasm", + "bytes": 1565374, + "kind": "wasm", + "gzip_bytes": 492040 + }, + { + "label": "tsv_wasm", + "bytes": 2881851, + "kind": "wasm", + "gzip_bytes": 920193 + }, + { + "label": "biome (wasm)", + "bytes": 34430368, + "kind": "wasm", + "gzip_bytes": 8187702 + }, + { + "label": "oxc-parser (napi)", + "bytes": 2658008, + "kind": "native", + "gzip_bytes": 1032774 + }, + { + "label": "oxfmt (napi)", + "bytes": 8016464, + "kind": "native", + "gzip_bytes": 3224684 + }, + { + "label": "oxc-parser (wasm)", + "bytes": 1866798, + "kind": "wasm", + "gzip_bytes": 518682 + } + ], + "entries": [ + { + "name": "svelte/compiler", + "group": "parse/svelte", + "mean_ns": 446701654.3636364, + "p50_ns": 447117610, + "p75_ns": 449357427, + "p90_ns": 452683243.5, + "p95_ns": 460334045.34999996, + "p99_ns": 467667861.07, + "min_ns": 441902583, + "max_ns": 469501315, + "std_dev_ns": 3453239.370632545, + "cv": 0.007730527381977064, + "ops_per_second": 2.238630616724675, + "sample_size": 11, + "files_processed": 1211, + "files_total": 1258, + "files_iterated": 1209, + "runtime": "deno" + }, + { + "name": "tsv-json", + "group": "parse/svelte", + "mean_ns": 576417325.5555556, + "p50_ns": 575146175, + "p75_ns": 580786009, + "p90_ns": 587896561.8, + "p95_ns": 590511267.4, + "p99_ns": 592603031.88, + "min_ns": 563710286, + "max_ns": 593125973, + "std_dev_ns": 9327150.62643844, + "cv": 0.016181246143926815, + "ops_per_second": 1.7348541684381054, + "sample_size": 9, + "files_processed": 1219, + "files_total": 1258, + "files_iterated": 1209, + "runtime": "deno" + }, + { + "name": "tsv_wasm-json", + "group": "parse/svelte", + "mean_ns": 604529120.7777778, + "p50_ns": 600610111, + "p75_ns": 609972133, + "p90_ns": 616789621.4, + "p95_ns": 617223030.2, + "p99_ns": 617569757.24, + "min_ns": 596534979, + "max_ns": 617656439, + "std_dev_ns": 8249194.136839803, + "cv": 0.013645652216433376, + "ops_per_second": 1.654180031415882, + "sample_size": 9, + "files_processed": 1219, + "files_total": 1258, + "files_iterated": 1209, + "runtime": "deno" + }, + { + "name": "tsv-internal", + "group": "parse/svelte", + "mean_ns": 33331912.507462688, + "p50_ns": 33787409, + "p75_ns": 34614749, + "p90_ns": 35501176.199999996, + "p95_ns": 44068832.59999992, + "p99_ns": 56688693.52, + "min_ns": 29286644, + "max_ns": 62292028, + "std_dev_ns": 1463577.6860925104, + "cv": 0.04390920220268878, + "ops_per_second": 30.00127879779205, + "sample_size": 134, + "files_processed": 1219, + "files_total": 1258, + "files_iterated": 1209, + "runtime": "deno" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/svelte", + "mean_ns": 44796253.053571425, + "p50_ns": 44550250, + "p75_ns": 45627355.25, + "p90_ns": 46152637.4, + "p95_ns": 46448952.199999996, + "p99_ns": 47144887.02, + "min_ns": 43540486, + "max_ns": 47220494, + "std_dev_ns": 942303.4521598811, + "cv": 0.02103531853507902, + "ops_per_second": 22.32329562930429, + "sample_size": 112, + "files_processed": 1219, + "files_total": 1258, + "files_iterated": 1209, + "runtime": "deno" + }, + { + "name": "prettier", + "group": "format/svelte", + "mean_ns": 4225150791, + "p50_ns": 4229227845, + "p75_ns": 4256976509.5, + "p90_ns": 4277263348.2, + "p95_ns": 4277612615.1, + "p99_ns": 4277892028.62, + "min_ns": 4151832182, + "max_ns": 4277961882, + "std_dev_ns": 47404302.42223423, + "cv": 0.011219552808200408, + "ops_per_second": 0.23667794345472865, + "sample_size": 7, + "files_processed": 1219, + "files_total": 1258, + "files_iterated": 1208, + "runtime": "deno" + }, + { + "name": "tsv", + "group": "format/svelte", + "mean_ns": 346987250.06666666, + "p50_ns": 346708116, + "p75_ns": 350955933, + "p90_ns": 356269843.6, + "p95_ns": 362670100.9, + "p99_ns": 371121320.18, + "min_ns": 331912632, + "max_ns": 373234125, + "std_dev_ns": 10304760.625883868, + "cv": 0.029697807697268458, + "ops_per_second": 2.8819502728352986, + "sample_size": 15, + "files_processed": 1219, + "files_total": 1258, + "files_iterated": 1208, + "runtime": "deno" + }, + { + "name": "tsv_wasm", + "group": "format/svelte", + "mean_ns": 388091709.3333333, + "p50_ns": 386351724, + "p75_ns": 396924137, + "p90_ns": 406582824.4, + "p95_ns": 409538314.4, + "p99_ns": 412825474.88, + "min_ns": 378662728, + "max_ns": 413647265, + "std_dev_ns": 9750093.460261889, + "cv": 0.025123168637151947, + "ops_per_second": 2.576710545344571, + "sample_size": 12, + "files_processed": 1219, + "files_total": 1258, + "files_iterated": 1208, + "runtime": "deno" + }, + { + "name": "oxfmt", + "group": "format/svelte", + "mean_ns": 4249592319, + "p50_ns": 4245751623, + "p75_ns": 4281939960, + "p90_ns": 4355658840.6, + "p95_ns": 4387654716.3, + "p99_ns": 4413251416.86, + "min_ns": 4214289558, + "max_ns": 4419650592, + "std_dev_ns": 33808151.84827225, + "cv": 0.00795562240102784, + "ops_per_second": 0.23531669038674202, + "sample_size": 6, + "files_processed": 1216, + "files_total": 1258, + "files_iterated": 1208, + "runtime": "deno" + }, + { + "name": "biome-wasm", + "group": "format/svelte", + "mean_ns": 795829870.4, + "p50_ns": 796139174, + "p75_ns": 798224570, + "p90_ns": 802942563.8, + "p95_ns": 805226452.4, + "p99_ns": 807053563.28, + "min_ns": 794723414, + "max_ns": 807510341, + "std_dev_ns": 678990.6083550052, + "cv": 0.0008531856287496861, + "ops_per_second": 1.256549970281186, + "sample_size": 5, + "files_processed": 1256, + "files_total": 1258, + "files_iterated": 1208, + "runtime": "deno" + }, + { + "name": "acorn-typescript", + "group": "parse/typescript", + "mean_ns": 2943567326.6, + "p50_ns": 2947136359, + "p75_ns": 2948841183, + "p90_ns": 2949914680.8, + "p95_ns": 2950272513.4, + "p99_ns": 2950558779.48, + "min_ns": 2931955339, + "max_ns": 2950630346, + "std_dev_ns": 7805667.921236869, + "cv": 0.002651771491924016, + "ops_per_second": 0.33972384153178553, + "sample_size": 5, + "files_processed": 4189, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "deno" + }, + { + "name": "tsv-json", + "group": "parse/typescript", + "mean_ns": 3229347215.2, + "p50_ns": 3220211719, + "p75_ns": 3242398496, + "p90_ns": 3263545765.4, + "p95_ns": 3270594855.2, + "p99_ns": 3276234127.04, + "min_ns": 3193003603, + "max_ns": 3277643945, + "std_dev_ns": 32245338.948024817, + "cv": 0.009985095066969377, + "ops_per_second": 0.30966010569974217, + "sample_size": 5, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "deno" + }, + { + "name": "tsv_wasm-json", + "group": "parse/typescript", + "mean_ns": 2959373742.4, + "p50_ns": 2965562933, + "p75_ns": 2981368060, + "p90_ns": 2981524637.2, + "p95_ns": 2981576829.6, + "p99_ns": 2981618583.52, + "min_ns": 2916054825, + "max_ns": 2981629022, + "std_dev_ns": 27131967.843614813, + "cv": 0.009168145089241504, + "ops_per_second": 0.33790933050214117, + "sample_size": 5, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "deno" + }, + { + "name": "tsv-internal", + "group": "parse/typescript", + "mean_ns": 227619992.82608697, + "p50_ns": 224724080, + "p75_ns": 235532212, + "p90_ns": 242167218, + "p95_ns": 243441323.9, + "p99_ns": 247124533.98000002, + "min_ns": 217155381, + "max_ns": 248136290, + "std_dev_ns": 9768994.514804596, + "cv": 0.04291799851812048, + "ops_per_second": 4.393287195839822, + "sample_size": 23, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "deno" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/typescript", + "mean_ns": 322840917.75, + "p50_ns": 322471236.5, + "p75_ns": 324960526.25, + "p90_ns": 327035633, + "p95_ns": 327176235.75, + "p99_ns": 327352253.55, + "min_ns": 317554353, + "max_ns": 327396258, + "std_dev_ns": 3041015.4308460737, + "cv": 0.009419547720406868, + "ops_per_second": 3.0975007968920942, + "sample_size": 16, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "deno" + }, + { + "name": "oxc-parser", + "group": "parse/typescript", + "mean_ns": 1182205886.2, + "p50_ns": 1179753189, + "p75_ns": 1209995476, + "p90_ns": 1212859618, + "p95_ns": 1213814332, + "p99_ns": 1214578103.2, + "min_ns": 1147892413, + "max_ns": 1214769046, + "std_dev_ns": 29884684.883617558, + "cv": 0.025278748171079404, + "ops_per_second": 0.8458763500275998, + "sample_size": 5, + "files_processed": 4329, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "deno" + }, + { + "name": "oxc-parser-wasm", + "group": "parse/typescript", + "mean_ns": 1287702416, + "p50_ns": 1288018352, + "p75_ns": 1289532756, + "p90_ns": 1290665682, + "p95_ns": 1291043324, + "p99_ns": 1291345437.6, + "min_ns": 1282526859, + "max_ns": 1291420966, + "std_dev_ns": 3336879.3875717926, + "cv": 0.0025913435791610666, + "ops_per_second": 0.7765769385649736, + "sample_size": 5, + "files_processed": 4521, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "deno" + }, + { + "name": "prettier", + "group": "format/typescript", + "mean_ns": 12545992146.285715, + "p50_ns": 12519655984, + "p75_ns": 12612682034, + "p90_ns": 12706215702.4, + "p95_ns": 12734593173.7, + "p99_ns": 12757295150.74, + "min_ns": 12350594851, + "max_ns": 12762970645, + "std_dev_ns": 135099202.27778918, + "cv": 0.01076831554671312, + "ops_per_second": 0.07970672931562878, + "sample_size": 7, + "files_processed": 4351, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "deno" + }, + { + "name": "tsv", + "group": "format/typescript", + "mean_ns": 932350504.3333334, + "p50_ns": 932137113.5, + "p75_ns": 936177901.5, + "p90_ns": 938326763, + "p95_ns": 939179896.5, + "p99_ns": 939862403.3, + "min_ns": 925222708, + "max_ns": 940033030, + "std_dev_ns": 5695844.775467416, + "cv": 0.00610912392817352, + "ops_per_second": 1.0725580083372601, + "sample_size": 6, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "deno" + }, + { + "name": "tsv_wasm", + "group": "format/typescript", + "mean_ns": 1214055361, + "p50_ns": 1210383430, + "p75_ns": 1217189611, + "p90_ns": 1228277329, + "p95_ns": 1231973235, + "p99_ns": 1234929959.8, + "min_ns": 1199355730, + "max_ns": 1235669141, + "std_dev_ns": 13667484.972653583, + "cv": 0.011257711478161812, + "ops_per_second": 0.8236856671645635, + "sample_size": 5, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "deno" + }, + { + "name": "oxfmt", + "group": "format/typescript", + "mean_ns": 1083163537.6, + "p50_ns": 1090211451, + "p75_ns": 1091132508, + "p90_ns": 1111234014.6, + "p95_ns": 1117934516.8, + "p99_ns": 1123294918.56, + "min_ns": 1045487082, + "max_ns": 1124635019, + "std_dev_ns": 30026923.716132935, + "cv": 0.02772150527026099, + "ops_per_second": 0.9232216237778202, + "sample_size": 5, + "files_processed": 4332, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "deno" + }, + { + "name": "biome-wasm", + "group": "format/typescript", + "mean_ns": 4922622328.142858, + "p50_ns": 4916969834, + "p75_ns": 4929270215, + "p90_ns": 4944277294.8, + "p95_ns": 4955245293.9, + "p99_ns": 4964019693.18, + "min_ns": 4894936754, + "max_ns": 4966213293, + "std_dev_ns": 22842608.897778206, + "cv": 0.004640333418874319, + "ops_per_second": 0.20314375821255962, + "sample_size": 7, + "files_processed": 4521, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "deno" + }, + { + "name": "svelte/compiler", + "group": "parse/css", + "mean_ns": 5280178.255033557, + "p50_ns": 5182711, + "p75_ns": 5542048, + "p90_ns": 5769681.2, + "p95_ns": 5896509.699999999, + "p99_ns": 6380541.36, + "min_ns": 4978405, + "max_ns": 7135021, + "std_dev_ns": 256468.79159355842, + "cv": 0.048571994960410377, + "ops_per_second": 189.38754559028513, + "sample_size": 894, + "files_processed": 152, + "files_total": 182, + "files_iterated": 148, + "runtime": "deno" + }, + { + "name": "tsv-json", + "group": "parse/css", + "mean_ns": 15038254.332155477, + "p50_ns": 14993863, + "p75_ns": 15562049.5, + "p90_ns": 17413281.90000001, + "p95_ns": 19977118.05, + "p99_ns": 20722291.04, + "min_ns": 14365814, + "max_ns": 21706353, + "std_dev_ns": 487079.93606330716, + "cv": 0.03238939342991498, + "ops_per_second": 66.4970799078557, + "sample_size": 283, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "deno" + }, + { + "name": "tsv_wasm-json", + "group": "parse/css", + "mean_ns": 16418291.885017421, + "p50_ns": 16333013, + "p75_ns": 16713440, + "p90_ns": 17030068.2, + "p95_ns": 17450256.599999998, + "p99_ns": 18611535.920000006, + "min_ns": 16065338, + "max_ns": 19845444, + "std_dev_ns": 324352.159050538, + "cv": 0.019755536161866316, + "ops_per_second": 60.90767584127031, + "sample_size": 287, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "deno" + }, + { + "name": "tsv-internal", + "group": "parse/css", + "mean_ns": 5567489.457420925, + "p50_ns": 5546787, + "p75_ns": 5685176, + "p90_ns": 5958815.2, + "p95_ns": 6216783.899999999, + "p99_ns": 11471542.929999959, + "min_ns": 5246041, + "max_ns": 23210729, + "std_dev_ns": 178748.87818777648, + "cv": 0.032105831462243994, + "ops_per_second": 179.61417038106768, + "sample_size": 822, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "deno" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/css", + "mean_ns": 7650987.573578595, + "p50_ns": 7638560, + "p75_ns": 7735724, + "p90_ns": 7906341.600000001, + "p95_ns": 8179290.2, + "p99_ns": 8653222.159999998, + "min_ns": 7511862, + "max_ns": 10208716, + "std_dev_ns": 103151.41883586982, + "cv": 0.01348210513268718, + "ops_per_second": 130.70208131736257, + "sample_size": 598, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "deno" + }, + { + "name": "prettier", + "group": "format/css", + "mean_ns": 249725077.0526316, + "p50_ns": 248377169, + "p75_ns": 253842403, + "p90_ns": 257923203.9, + "p95_ns": 266488723.25, + "p99_ns": 278278211.84999996, + "min_ns": 240709739, + "max_ns": 281225584, + "std_dev_ns": 5980518.595691874, + "cv": 0.02394841025289355, + "ops_per_second": 4.004403609771404, + "sample_size": 19, + "files_processed": 181, + "files_total": 182, + "files_iterated": 154, + "runtime": "deno" + }, + { + "name": "tsv", + "group": "format/css", + "mean_ns": 12624667.18230563, + "p50_ns": 12490889, + "p75_ns": 13377320, + "p90_ns": 14051372.6, + "p95_ns": 14673515.6, + "p99_ns": 28249243.47999994, + "min_ns": 10881775, + "max_ns": 30497518, + "std_dev_ns": 957106.4667485798, + "cv": 0.07581241175926068, + "ops_per_second": 79.21000891029992, + "sample_size": 373, + "files_processed": 154, + "files_total": 182, + "files_iterated": 154, + "runtime": "deno" + }, + { + "name": "tsv_wasm", + "group": "format/css", + "mean_ns": 14961824.314102564, + "p50_ns": 14892973.5, + "p75_ns": 15194578.5, + "p90_ns": 15615153.000000002, + "p95_ns": 16071482.65, + "p99_ns": 17014704.93, + "min_ns": 14604112, + "max_ns": 30174958, + "std_dev_ns": 296727.4630211618, + "cv": 0.01983230499114172, + "ops_per_second": 66.83676930074832, + "sample_size": 312, + "files_processed": 154, + "files_total": 182, + "files_iterated": 154, + "runtime": "deno" + }, + { + "name": "oxfmt", + "group": "format/css", + "mean_ns": 277591089.6875, + "p50_ns": 276996841, + "p75_ns": 282220759, + "p90_ns": 289822308.7, + "p95_ns": 295245221.15, + "p99_ns": 297749009.03, + "min_ns": 271861149, + "max_ns": 298374956, + "std_dev_ns": 4872724.206840176, + "cv": 0.017553604520684284, + "ops_per_second": 3.6024211048191663, + "sample_size": 16, + "files_processed": 181, + "files_total": 182, + "files_iterated": 154, + "runtime": "deno" + }, + { + "name": "biome-wasm", + "group": "format/css", + "mean_ns": 67563688.5890411, + "p50_ns": 67445756.5, + "p75_ns": 70102786, + "p90_ns": 71322491.7, + "p95_ns": 73752630.24999999, + "p99_ns": 78082292.60999995, + "min_ns": 63140649, + "max_ns": 85839523, + "std_dev_ns": 3077594.8066685484, + "cv": 0.04555101817173045, + "ops_per_second": 14.800849700236778, + "sample_size": 73, + "files_processed": 182, + "files_total": 182, + "files_iterated": 154, + "runtime": "deno" + } + ], + "suppressed_noise": { + "oxfmt::textToDoc": 1, + "panicked at crates/biome_rowan": 1 + } +} \ No newline at end of file diff --git a/benches/js/results/report.deno.md b/benches/js/results/report.deno.md new file mode 100644 index 000000000..dbe906bff --- /dev/null +++ b/benches/js/results/report.deno.md @@ -0,0 +1,197 @@ +# tsv benchmark results + +**Runtime:** deno + +**Date:** 2026-06-26T15:56:20.597Z — tsv 0.1.0 (e4c23f8e) + +**Corpus:** 1258 Svelte (1.8 MB), 4521 TypeScript (16.2 MB), 182 CSS (0.2 MB) — 5961 files, 18.2 MB total + +**Versions:** svelte@5.56.1, acorn@8.16.0, acorn-typescript@1.0.10, prettier@3.8.3, prettier-plugin-svelte@4.1.1, oxc-parser@0.134.0, oxfmt@0.53.0, @biomejs/wasm-bundler@2.4.16 + +**Methodology:** Single-threaded — every implementation formats/parses one file at a time, measured sequentially with no cross-file parallelism. The numbers are per-file, single-core latency/throughput, not the multi-core batch throughput a CLI gets formatting many files at once. + +## parse/svelte + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs svelte/compiler (speedup) | +| ----------------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------------------------- | +| svelte/compiler | 2.24 | 11 | 447.12 | 449.36 | 452.68 | 460.33 | 467.67 | 441.90 | 469.50 | baseline | +| tsv-json | 1.73 | 9 | 575.15 | 580.79 | 587.90 | — | — | 563.71 | 593.13 | 0.77x | +| tsv_wasm-json | 1.65 | 9 | 600.61 | 609.97 | 616.79 | — | — | 596.53 | 617.66 | 0.74x | +| tsv-internal | 30.00 | 134 | 33.79 | 34.61 | 35.50 | 44.07 | 56.69 | 29.29 | 62.29 | 13.4x | +| tsv_wasm-internal | 22.32 | 112 | 44.55 | 45.63 | 46.15 | 46.45 | 47.14 | 43.54 | 47.22 | 9.97x | + +**Files (intersection):** 1209 + +**Throughput:** svelte/compiler 3.9 MB/s, tsv-json 3.0 MB/s, tsv_wasm-json 2.9 MB/s, tsv-internal 52.5 MB/s, tsv_wasm-internal 39.0 MB/s + +**Coverage:** svelte/compiler 1211/1258 (96%), tsv-json 1219/1258 (96%), tsv_wasm-json 1219/1258 (96%), tsv-internal 1219/1258 (96%), tsv_wasm-internal 1219/1258 (96%) + +**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 17.3x tsv-internal, tsv_wasm-json 13.5x tsv_wasm-internal + +## format/svelte + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs prettier (speedup) | +| ---------- | ------- | -- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | --------------------- | +| prettier | 0.24 | 7 | 4229.23 | 4256.98 | 4277.26 | — | — | 4151.83 | 4277.96 | baseline | +| tsv | 2.88 | 15 | 346.71 | 350.96 | 356.27 | 362.67 | 371.12 | 331.91 | 373.23 | 12.2x | +| tsv_wasm | 2.58 | 12 | 386.35 | 396.92 | 406.58 | 409.54 | 412.83 | 378.66 | 413.65 | 10.9x | +| oxfmt | 0.24 | 6 | 4245.75 | 4281.94 | 4355.66 | — | — | 4214.29 | 4419.65 | 0.99x | +| biome-wasm | 1.26 | 5 | 796.14 | 798.22 | 802.94 | — | — | 794.72 | 807.51 | 5.31x | + +**Files (intersection):** 1208 + +**Throughput:** prettier 0.4 MB/s, tsv 4.9 MB/s, tsv_wasm 4.3 MB/s, oxfmt 0.4 MB/s, biome-wasm 2.1 MB/s + +**Coverage:** prettier 1219/1258 (96%), tsv 1219/1258 (96%), tsv_wasm 1219/1258 (96%), oxfmt 1216/1258 (96%), biome-wasm 1256/1258 (99%) + +## parse/typescript + +| Task Name | ops/sec | n | p50 (s) | p75 (s) | p90 (s) | p95 (s) | p99 (s) | min (s) | max (s) | vs acorn-typescript (speedup) | +| ----------------- | ------- | -- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ----------------------------- | +| acorn-typescript | 0.34 | 5 | 2.95 | 2.95 | 2.95 | — | — | 2.93 | 2.95 | baseline | +| tsv-json | 0.31 | 5 | 3.22 | 3.24 | 3.26 | — | — | 3.19 | 3.28 | 0.91x | +| tsv_wasm-json | 0.34 | 5 | 2.97 | 2.98 | 2.98 | — | — | 2.92 | 2.98 | 0.99x | +| tsv-internal | 4.39 | 23 | 0.22 | 0.24 | 0.24 | 0.24 | 0.25 | 0.22 | 0.25 | 12.9x | +| tsv_wasm-internal | 3.10 | 16 | 0.32 | 0.32 | 0.33 | 0.33 | 0.33 | 0.32 | 0.33 | 9.12x | +| oxc-parser | 0.85 | 5 | 1.18 | 1.21 | 1.21 | — | — | 1.15 | 1.21 | 2.49x | +| oxc-parser-wasm | 0.78 | 5 | 1.29 | 1.29 | 1.29 | — | — | 1.28 | 1.29 | 2.29x | + +**Files (intersection):** 4120 + +**Throughput:** acorn-typescript 5.4 MB/s, tsv-json 5.0 MB/s, tsv_wasm-json 5.4 MB/s, tsv-internal 70.3 MB/s, tsv_wasm-internal 49.6 MB/s, oxc-parser 13.5 MB/s, oxc-parser-wasm 12.4 MB/s + +**Coverage:** acorn-typescript 4189/4521 (92%), tsv-json 4260/4521 (94%), tsv_wasm-json 4260/4521 (94%), tsv-internal 4260/4521 (94%), tsv_wasm-internal 4260/4521 (94%), oxc-parser 4329/4521 (95%), oxc-parser-wasm 4521/4521 (100%) + +**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 14.2x tsv-internal, tsv_wasm-json 9.2x tsv_wasm-internal + +## format/typescript + +| Task Name | ops/sec | n | p50 (s) | p75 (s) | p90 (s) | p95 (s) | p99 (s) | min (s) | max (s) | vs prettier (speedup) | +| ---------- | ------- | - | ------- | ------- | ------- | ------- | ------- | ------- | ------- | --------------------- | +| prettier | 0.08 | 7 | 12.52 | 12.61 | 12.71 | — | — | 12.35 | 12.76 | baseline | +| tsv | 1.07 | 6 | 0.93 | 0.94 | 0.94 | — | — | 0.93 | 0.94 | 13.5x | +| tsv_wasm | 0.82 | 5 | 1.21 | 1.22 | 1.23 | — | — | 1.20 | 1.24 | 10.3x | +| oxfmt | 0.92 | 5 | 1.09 | 1.09 | 1.11 | — | — | 1.05 | 1.12 | 11.6x | +| biome-wasm | 0.20 | 7 | 4.92 | 4.93 | 4.94 | — | — | 4.89 | 4.97 | 2.55x | + +**Files (intersection):** 4219 + +**Throughput:** prettier 1.3 MB/s, tsv 17.2 MB/s, tsv_wasm 13.2 MB/s, oxfmt 14.8 MB/s, biome-wasm 3.3 MB/s + +**Coverage:** prettier 4351/4521 (96%), tsv 4260/4521 (94%), tsv_wasm 4260/4521 (94%), oxfmt 4332/4521 (95%), biome-wasm 4521/4521 (100%) + +## parse/css + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs svelte/compiler (speedup) | +| ----------------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------------------------- | +| svelte/compiler | 189.39 | 894 | 5.18 | 5.54 | 5.77 | 5.90 | 6.38 | 4.98 | 7.14 | baseline | +| tsv-json | 66.50 | 283 | 14.99 | 15.56 | 17.41 | 19.98 | 20.72 | 14.37 | 21.71 | 0.35x | +| tsv_wasm-json | 60.91 | 287 | 16.33 | 16.71 | 17.03 | 17.45 | 18.61 | 16.07 | 19.85 | 0.32x | +| tsv-internal | 179.61 | 822 | 5.55 | 5.69 | 5.96 | 6.22 | 11.47 | 5.25 | 23.21 | 0.95x | +| tsv_wasm-internal | 130.70 | 598 | 7.64 | 7.74 | 7.91 | 8.18 | 8.65 | 7.51 | 10.21 | 0.69x | + +**Files (intersection):** 148 + +**Throughput:** svelte/compiler 34.6 MB/s, tsv-json 12.1 MB/s, tsv_wasm-json 11.1 MB/s, tsv-internal 32.8 MB/s, tsv_wasm-internal 23.9 MB/s + +**Coverage:** svelte/compiler 152/182 (83%), tsv-json 154/182 (84%), tsv_wasm-json 154/182 (84%), tsv-internal 154/182 (84%), tsv_wasm-internal 154/182 (84%) + +**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 2.7x tsv-internal, tsv_wasm-json 2.1x tsv_wasm-internal + +## format/css + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs prettier (speedup) | +| ---------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | --------------------- | +| prettier | 4.00 | 19 | 248.38 | 253.84 | 257.92 | 266.49 | 278.28 | 240.71 | 281.23 | baseline | +| tsv | 79.21 | 373 | 12.49 | 13.38 | 14.05 | 14.67 | 28.25 | 10.88 | 30.50 | 19.8x | +| tsv_wasm | 66.84 | 312 | 14.89 | 15.19 | 15.62 | 16.07 | 17.01 | 14.60 | 30.17 | 16.7x | +| oxfmt | 3.60 | 16 | 277.00 | 282.22 | 289.82 | 295.25 | 297.75 | 271.86 | 298.37 | 0.90x | +| biome-wasm | 14.80 | 73 | 67.45 | 70.10 | 71.32 | 73.75 | 78.08 | 63.14 | 85.84 | 3.70x | + +**Files (intersection):** 154 + +**Throughput:** prettier 0.8 MB/s, tsv 15.3 MB/s, tsv_wasm 12.9 MB/s, oxfmt 0.7 MB/s, biome-wasm 2.9 MB/s + +**Coverage:** prettier 181/182 (99%), tsv 154/182 (84%), tsv_wasm 154/182 (84%), oxfmt 181/182 (99%), biome-wasm 182/182 (100%) + +_Note: every `Nx` is speedup form — values > 1 mean self is faster. File counts come from the per-group `Files (intersection):` / `Coverage:` lines and the Comparisons table row labels._ + +## Binary Sizes + +| Binary | Size | Gzipped | vs tsv | vs tsv (gz) | +| --- | ---: | ---: | ---: | ---: | +| tsv_format_wasm | 2.2 MB | 735.1 KB | 0.8x | 0.8x | +| tsv_parse_wasm | 1.6 MB | 492.0 KB | 0.5x | 0.5x | +| tsv_wasm | 2.9 MB | 920.2 KB | — | — | +| biome (wasm) | 34.4 MB | 8.2 MB | 11.9x | 8.9x | +| oxc-parser (wasm) | 1.9 MB | 518.7 KB | 0.6x | 0.6x | +| tsv (ffi) | 3.7 MB | 1.6 MB | — | — | +| oxc-parser+oxfmt (napi) | 10.7 MB | 4.3 MB | 2.9x | 2.7x | +| tsv format (ffi) | 2.9 MB | 1.3 MB | 0.8x | 0.8x | +| tsv parse (ffi) | 2.1 MB | 867.3 KB | 0.6x | 0.6x | +| tsv (napi) | 3.8 MB | 1.6 MB | 1.0x | 1.0x | +| oxc-parser (napi) | 2.7 MB | 1.0 MB | 0.7x | 0.7x | +| oxfmt (napi) | 8.0 MB | 3.2 MB | 2.2x | 2.1x | + +_Gzipped ≈ npm-tarball wire size (`gzip -c`, system default level). `vs tsv (gz)` compares gzipped bytes; `vs tsv` compares raw on-disk bytes._ + +## Comparisons to tsv (speedup) + +| Benchmark | Comparisons | +| --- | --- | +| format svelte (1208f) | **12.2x** prettier, **12.2x** oxfmt | +| format typescript (4219f) | **13.5x** prettier, **1.16x** oxfmt | +| format css (154f) | **19.8x** prettier, **22.0x** oxfmt | +| parse svelte (1209f) | **0.77x** svelte | +| parse typescript (4120f) | **0.91x** svelte, **0.37x** oxc-parser | +| parse css (148f) | **0.35x** svelte | + +## Comparisons to tsv_wasm (speedup) + +| Benchmark | Comparisons | +| --- | --- | +| format svelte (1208f) | **10.9x** prettier, **2.05x** biome-wasm | +| format typescript (4219f) | **10.3x** prettier, **4.05x** biome-wasm | +| format css (154f) | **16.7x** prettier, **4.52x** biome-wasm | +| parse svelte (1209f) | **0.74x** svelte | +| parse typescript (4120f) | **0.99x** svelte, **0.44x** oxc-parser-wasm | +| parse css (148f) | **0.32x** svelte | + +_`Nx` is speedup — self is N× faster than the named opponent. `(Mf)` is the self impl's iterated count (per-group intersection in default mode; per-impl success set in `BENCH_MODE=union`). Parse canonical: svelte/compiler for .svelte/.css, acorn-typescript for .ts. oxc-parser (native and wasm) serializes the AST to JSON in Rust and deserializes it in JS — the same eager materialization as tsv-json/tsv_wasm-json, so these parse rows are apples-to-apples. tsv-internal/tsv_wasm-internal are parse-only (no JS materialization) and have no oxc counterpart — oxc exposes no comparably cheap mode (its JS API always serializes; experimentalLazy is setup-dominated). Format groups include parse time — each formatter parses internally._ + +## Skipped Files + +1373 unique file+error combinations — Svelte 169, TypeScript 1144, CSS 60. + +**Per-benchmark skip counts:** +- parse/typescript: acorn-typescript: 332 +- parse/typescript: tsv-json: 261 +- parse/typescript: tsv_wasm-json: 261 +- parse/typescript: tsv-internal: 261 +- parse/typescript: tsv_wasm-internal: 261 +- format/typescript: tsv: 261 +- format/typescript: tsv_wasm: 261 +- parse/typescript: oxc-parser: 192 +- format/typescript: oxfmt: 189 +- format/typescript: prettier: 170 +- parse/svelte: svelte/compiler: 47 +- format/svelte: oxfmt: 42 +- parse/svelte: tsv-json: 39 +- parse/svelte: tsv_wasm-json: 39 +- parse/svelte: tsv-internal: 39 +- parse/svelte: tsv_wasm-internal: 39 +- format/svelte: prettier: 39 +- format/svelte: tsv: 39 +- format/svelte: tsv_wasm: 39 +- parse/css: svelte/compiler: 30 +- parse/css: tsv-json: 28 +- parse/css: tsv_wasm-json: 28 +- parse/css: tsv-internal: 28 +- parse/css: tsv_wasm-internal: 28 +- format/css: tsv: 28 +- format/css: tsv_wasm: 28 +- format/svelte: biome-wasm: 2 +- format/css: prettier: 1 +- format/css: oxfmt: 1 + +_Per-file detail omitted. Re-run with `--verbose` to include error messages and failure sets per file._ diff --git a/benches/js/results/report.json b/benches/js/results/report.json new file mode 100644 index 000000000..e7a67ac0c --- /dev/null +++ b/benches/js/results/report.json @@ -0,0 +1,628 @@ +{ + "version": 5, + "kind": "combined", + "generated": "2026-06-26T16:14:57.526Z", + "runtimes": [ + "deno", + "node", + "bun" + ], + "sources": [ + { + "runtime": "deno", + "timestamp": "2026-06-26T15:56:20.597Z", + "git_commit": "e4c23f8e", + "tsv": "0.1.0" + }, + { + "runtime": "node", + "timestamp": "2026-06-26T16:05:52.967Z", + "git_commit": "e4c23f8e", + "tsv": "0.1.0" + }, + { + "runtime": "bun", + "timestamp": "2026-06-26T16:14:57.136Z", + "git_commit": "e4c23f8e", + "tsv": "0.1.0" + } + ], + "rows": [ + { + "group": "parse/svelte", + "name": "svelte/compiler", + "ops_per_second": { + "deno": 2.238630616724675, + "node": 2.171459192454959, + "bun": 1.3719971999128666 + }, + "mean_ns": { + "deno": 446701654.3636364, + "node": 460519821.6363636, + "bun": 728864461.2857143 + }, + "files_iterated": { + "deno": 1209, + "node": 1210, + "bun": 1210 + } + }, + { + "group": "parse/svelte", + "name": "tsv-json", + "ops_per_second": { + "deno": 1.7348541684381054, + "node": 1.8194660999068581, + "bun": 2.117882505475197 + }, + "mean_ns": { + "deno": 576417325.5555556, + "node": 549611779, + "bun": 472169724.90909094 + }, + "files_iterated": { + "deno": 1209, + "node": 1210, + "bun": 1210 + } + }, + { + "group": "parse/svelte", + "name": "tsv_wasm-json", + "ops_per_second": { + "deno": 1.654180031415882, + "node": 1.8438317886968851, + "bun": 2.0787300384189034 + }, + "mean_ns": { + "deno": 604529120.7777778, + "node": 542348822.7777778, + "bun": 481062947.8181818 + }, + "files_iterated": { + "deno": 1209, + "node": 1210, + "bun": 1210 + } + }, + { + "group": "parse/svelte", + "name": "tsv-internal", + "ops_per_second": { + "deno": 30.00127879779205, + "node": 35.93875569027329, + "bun": 38.41372651816012 + }, + "mean_ns": { + "deno": 33331912.507462688, + "node": 27825114.72067039, + "bun": 26032361.102150537 + }, + "files_iterated": { + "deno": 1209, + "node": 1210, + "bun": 1210 + } + }, + { + "group": "parse/svelte", + "name": "tsv_wasm-internal", + "ops_per_second": { + "deno": 22.32329562930429, + "node": 25.936879910058455, + "bun": 27.381700251347603 + }, + "mean_ns": { + "deno": 44796253.053571425, + "node": 38555138.608333334, + "bun": 36520741.62015504 + }, + "files_iterated": { + "deno": 1209, + "node": 1210, + "bun": 1210 + } + }, + { + "group": "format/svelte", + "name": "prettier", + "ops_per_second": { + "deno": 0.23667794345472865, + "node": 0.2264356250709842, + "bun": 0.21810325927088134 + }, + "mean_ns": { + "deno": 4225150791, + "node": 4416266211.142858, + "bun": 4584984210.428572 + }, + "files_iterated": { + "deno": 1208, + "node": 1209, + "bun": 1211 + } + }, + { + "group": "format/svelte", + "name": "tsv", + "ops_per_second": { + "deno": 2.8819502728352986, + "node": 4.127792696416839, + "bun": 3.8587202815978814 + }, + "mean_ns": { + "deno": 346987250.06666666, + "node": 242260228.05555555, + "bun": 259153275.44444445 + }, + "files_iterated": { + "deno": 1208, + "node": 1209, + "bun": 1211 + } + }, + { + "group": "format/svelte", + "name": "tsv_wasm", + "ops_per_second": { + "deno": 2.576710545344571, + "node": 2.8337283625922014, + "bun": 2.832828903462568 + }, + "mean_ns": { + "deno": 388091709.3333333, + "node": 352891975.53333336, + "bun": 353004023.21428573 + }, + "files_iterated": { + "deno": 1208, + "node": 1209, + "bun": 1211 + } + }, + { + "group": "format/svelte", + "name": "oxfmt", + "ops_per_second": { + "deno": 0.23531669038674202, + "node": 0.23006201546923422, + "bun": 0.18181135209599103 + }, + "mean_ns": { + "deno": 4249592319, + "node": 4346654087.857142, + "bun": 5500206606.857142 + }, + "files_iterated": { + "deno": 1208, + "node": 1209, + "bun": 1211 + } + }, + { + "group": "format/svelte", + "name": "biome-wasm", + "ops_per_second": { + "deno": 1.256549970281186, + "node": 1.4362701327134877 + }, + "mean_ns": { + "deno": 795829870.4, + "node": 696247855.625 + }, + "files_iterated": { + "deno": 1208, + "node": 1209 + } + }, + { + "group": "parse/typescript", + "name": "acorn-typescript", + "ops_per_second": { + "deno": 0.33972384153178553, + "node": 0.32505966965627475, + "bun": 0.15898909853547305 + }, + "mean_ns": { + "deno": 2943567326.6, + "node": 3076358260.8, + "bun": 6289739417.428572 + }, + "files_iterated": { + "deno": 4120, + "node": 4120, + "bun": 4120 + } + }, + { + "group": "parse/typescript", + "name": "tsv-json", + "ops_per_second": { + "deno": 0.30966010569974217, + "node": 0.3703073270840239, + "bun": 0.5098148870230883 + }, + "mean_ns": { + "deno": 3229347215.2, + "node": 2700459663.8, + "bun": 1961496271.4 + }, + "files_iterated": { + "deno": 4120, + "node": 4120, + "bun": 4120 + } + }, + { + "group": "parse/typescript", + "name": "tsv_wasm-json", + "ops_per_second": { + "deno": 0.33790933050214117, + "node": 0.3423814923427335, + "bun": 0.46423771599387137 + }, + "mean_ns": { + "deno": 2959373742.4, + "node": 2920718620.5, + "bun": 2154068843.5 + }, + "files_iterated": { + "deno": 4120, + "node": 4120, + "bun": 4120 + } + }, + { + "group": "parse/typescript", + "name": "tsv-internal", + "ops_per_second": { + "deno": 4.393287195839822, + "node": 4.815525788316548, + "bun": 5.272376675322944 + }, + "mean_ns": { + "deno": 227619992.82608697, + "node": 207661643.6, + "bun": 189667783.92 + }, + "files_iterated": { + "deno": 4120, + "node": 4120, + "bun": 4120 + } + }, + { + "group": "parse/typescript", + "name": "tsv_wasm-internal", + "ops_per_second": { + "deno": 3.0975007968920942, + "node": 3.7737717669487116, + "bun": 3.920307133301353 + }, + "mean_ns": { + "deno": 322840917.75, + "node": 264986878.31578946, + "bun": 255082055.0526316 + }, + "files_iterated": { + "deno": 4120, + "node": 4120, + "bun": 4120 + } + }, + { + "group": "parse/typescript", + "name": "oxc-parser", + "ops_per_second": { + "deno": 0.8458763500275998, + "node": 0.7780449976607323, + "bun": 1.0442646052841589 + }, + "mean_ns": { + "deno": 1182205886.2, + "node": 1285272706.6, + "bun": 957611696.25 + }, + "files_iterated": { + "deno": 4120, + "node": 4120, + "bun": 4120 + } + }, + { + "group": "parse/typescript", + "name": "oxc-parser-wasm", + "ops_per_second": { + "deno": 0.7765769385649736, + "node": 0.747772118968232 + }, + "mean_ns": { + "deno": 1287702416, + "node": 1337305811 + }, + "files_iterated": { + "deno": 4120, + "node": 4120 + } + }, + { + "group": "format/typescript", + "name": "prettier", + "ops_per_second": { + "deno": 0.07970672931562878, + "node": 0.07390887214419399, + "bun": 0.06404911345750294 + }, + "mean_ns": { + "deno": 12545992146.285715, + "node": 13530175349.571428, + "bun": 15613018604.285715 + }, + "files_iterated": { + "deno": 4219, + "node": 4219, + "bun": 4219 + } + }, + { + "group": "format/typescript", + "name": "tsv", + "ops_per_second": { + "deno": 1.0725580083372601, + "node": 1.2074651817407978, + "bun": 1.2414351255420437 + }, + "mean_ns": { + "deno": 932350504.3333334, + "node": 828181230.5, + "bun": 805519337.6 + }, + "files_iterated": { + "deno": 4219, + "node": 4219, + "bun": 4219 + } + }, + { + "group": "format/typescript", + "name": "tsv_wasm", + "ops_per_second": { + "deno": 0.8236856671645635, + "node": 0.9528432541414544, + "bun": 0.9578534407075834 + }, + "mean_ns": { + "deno": 1214055361, + "node": 1049490559.6, + "bun": 1044001052.25 + }, + "files_iterated": { + "deno": 4219, + "node": 4219, + "bun": 4219 + } + }, + { + "group": "format/typescript", + "name": "oxfmt", + "ops_per_second": { + "deno": 0.9232216237778202, + "node": 0.9584883942390313, + "bun": 0.7333963399702814 + }, + "mean_ns": { + "deno": 1083163537.6, + "node": 1043309450.6, + "bun": 1363519212.6 + }, + "files_iterated": { + "deno": 4219, + "node": 4219, + "bun": 4219 + } + }, + { + "group": "format/typescript", + "name": "biome-wasm", + "ops_per_second": { + "deno": 0.20314375821255962, + "node": 0.23656699434741207 + }, + "mean_ns": { + "deno": 4922622328.142858, + "node": 4227132372.2 + }, + "files_iterated": { + "deno": 4219, + "node": 4219 + } + }, + { + "group": "parse/css", + "name": "svelte/compiler", + "ops_per_second": { + "deno": 189.38754559028513, + "node": 176.21202057990953, + "bun": 136.16648168429685 + }, + "mean_ns": { + "deno": 5280178.255033557, + "node": 5674981.744769874, + "bun": 7343951.225225225 + }, + "files_iterated": { + "deno": 148, + "node": 148, + "bun": 148 + } + }, + { + "group": "parse/css", + "name": "tsv-json", + "ops_per_second": { + "deno": 66.4970799078557, + "node": 77.05402236749438, + "bun": 87.8934606924887 + }, + "mean_ns": { + "deno": 15038254.332155477, + "node": 12977907.827195467, + "bun": 11377410.69837587 + }, + "files_iterated": { + "deno": 148, + "node": 148, + "bun": 148 + } + }, + { + "group": "parse/css", + "name": "tsv_wasm-json", + "ops_per_second": { + "deno": 60.90767584127031, + "node": 65.9555718747639, + "bun": 71.16952761134718 + }, + "mean_ns": { + "deno": 16418291.885017421, + "node": 15161721.315960912, + "bun": 14050957.390934844 + }, + "files_iterated": { + "deno": 148, + "node": 148, + "bun": 148 + } + }, + { + "group": "parse/css", + "name": "tsv-internal", + "ops_per_second": { + "deno": 179.61417038106768, + "node": 207.33234728509868, + "bun": 220.58003846953548 + }, + "mean_ns": { + "deno": 5567489.457420925, + "node": 4823174.063740857, + "bun": 4533501.793445879 + }, + "files_iterated": { + "deno": 148, + "node": 148, + "bun": 148 + } + }, + { + "group": "parse/css", + "name": "tsv_wasm-internal", + "ops_per_second": { + "deno": 130.70208131736257, + "node": 150.61073385786818, + "bun": 150.96756743906028 + }, + "mean_ns": { + "deno": 7650987.573578595, + "node": 6639633.008784773, + "bun": 6623939.280227596 + }, + "files_iterated": { + "deno": 148, + "node": 148, + "bun": 148 + } + }, + { + "group": "format/css", + "name": "prettier", + "ops_per_second": { + "deno": 4.004403609771404, + "node": 3.793222204689036, + "bun": 3.918061399656031 + }, + "mean_ns": { + "deno": 249725077.0526316, + "node": 263628109.83333334, + "bun": 255228261.63157895 + }, + "files_iterated": { + "deno": 154, + "node": 154, + "bun": 154 + } + }, + { + "group": "format/css", + "name": "tsv", + "ops_per_second": { + "deno": 79.21000891029992, + "node": 101.06557130395603, + "bun": 105.63197034802205 + }, + "mean_ns": { + "deno": 12624667.18230563, + "node": 9894566.340425532, + "bun": 9466830.891304348 + }, + "files_iterated": { + "deno": 154, + "node": 154, + "bun": 154 + } + }, + { + "group": "format/css", + "name": "tsv_wasm", + "ops_per_second": { + "deno": 66.83676930074832, + "node": 77.3630631485335, + "bun": 77.99320050438536 + }, + "mean_ns": { + "deno": 14961824.314102564, + "node": 12926065.221590908, + "bun": 12821630.520775624 + }, + "files_iterated": { + "deno": 154, + "node": 154, + "bun": 154 + } + }, + { + "group": "format/css", + "name": "oxfmt", + "ops_per_second": { + "deno": 3.6024211048191663, + "node": 3.4602781656793393, + "bun": 2.845532359370373 + }, + "mean_ns": { + "deno": 277591089.6875, + "node": 288994107.4444444, + "bun": 351428089.26666665 + }, + "files_iterated": { + "deno": 154, + "node": 154, + "bun": 154 + } + }, + { + "group": "format/css", + "name": "biome-wasm", + "ops_per_second": { + "deno": 14.800849700236778, + "node": 18.248648278318363 + }, + "mean_ns": { + "deno": 67563688.5890411, + "node": 54798579.31111111 + }, + "files_iterated": { + "deno": 154, + "node": 154 + } + } + ] +} \ No newline at end of file diff --git a/benches/js/results/report.md b/benches/js/results/report.md new file mode 100644 index 000000000..d4bde8fd0 --- /dev/null +++ b/benches/js/results/report.md @@ -0,0 +1,69 @@ +# tsv benchmark results — cross-runtime + +**Generated:** 2026-06-26T16:14:57.526Z + +**Runtimes:** deno, node, bun — each runtime’s full report is its `report..{json,md}` sibling. + +A per-runtime delta on the same row is the signal: same engine, different runtime + binding boundary (Deno → FFI, Node/Bun → N-API). Ratios are vs `deno` (> 1 = faster than deno). + +## parse/svelte + +| Impl | deno ops/sec | node ops/sec | bun ops/sec | node/deno | bun/deno | +| --- | ---: | ---: | ---: | ---: | ---: | +| svelte/compiler | 2.2 | 2.2 | 1.4 | 0.97x | 0.61x | +| tsv-json | 1.7 | 1.8 | 2.1 | 1.05x | 1.22x | +| tsv_wasm-json | 1.7 | 1.8 | 2.1 | 1.11x | 1.26x | +| tsv-internal | 30.0 | 35.9 | 38.4 | 1.20x | 1.28x | +| tsv_wasm-internal | 22.3 | 25.9 | 27.4 | 1.16x | 1.23x | + +## format/svelte + +| Impl | deno ops/sec | node ops/sec | bun ops/sec | node/deno | bun/deno | +| --- | ---: | ---: | ---: | ---: | ---: | +| prettier | 0.2 | 0.2 | 0.2 | 0.96x | 0.92x | +| tsv | 2.9 | 4.1 | 3.9 | 1.43x | 1.34x | +| tsv_wasm | 2.6 | 2.8 | 2.8 | 1.10x | 1.10x | +| oxfmt | 0.2 | 0.2 | 0.2 | 0.98x | 0.77x | +| biome-wasm | 1.3 | 1.4 | — | 1.14x | — | + +## parse/typescript + +| Impl | deno ops/sec | node ops/sec | bun ops/sec | node/deno | bun/deno | +| --- | ---: | ---: | ---: | ---: | ---: | +| acorn-typescript | 0.3 | 0.3 | 0.2 | 0.96x | 0.47x | +| tsv-json | 0.3 | 0.4 | 0.5 | 1.20x | 1.65x | +| tsv_wasm-json | 0.3 | 0.3 | 0.5 | 1.01x | 1.37x | +| tsv-internal | 4.4 | 4.8 | 5.3 | 1.10x | 1.20x | +| tsv_wasm-internal | 3.1 | 3.8 | 3.9 | 1.22x | 1.27x | +| oxc-parser | 0.8 | 0.8 | 1.0 | 0.92x | 1.23x | +| oxc-parser-wasm | 0.8 | 0.7 | — | 0.96x | — | + +## format/typescript + +| Impl | deno ops/sec | node ops/sec | bun ops/sec | node/deno | bun/deno | +| --- | ---: | ---: | ---: | ---: | ---: | +| prettier | 0.1 | 0.1 | 0.1 | 0.93x | 0.80x | +| tsv | 1.1 | 1.2 | 1.2 | 1.13x | 1.16x | +| tsv_wasm | 0.8 | 1.0 | 1.0 | 1.16x | 1.16x | +| oxfmt | 0.9 | 1.0 | 0.7 | 1.04x | 0.79x | +| biome-wasm | 0.2 | 0.2 | — | 1.16x | — | + +## parse/css + +| Impl | deno ops/sec | node ops/sec | bun ops/sec | node/deno | bun/deno | +| --- | ---: | ---: | ---: | ---: | ---: | +| svelte/compiler | 189.4 | 176.2 | 136.2 | 0.93x | 0.72x | +| tsv-json | 66.5 | 77.1 | 87.9 | 1.16x | 1.32x | +| tsv_wasm-json | 60.9 | 66.0 | 71.2 | 1.08x | 1.17x | +| tsv-internal | 179.6 | 207.3 | 220.6 | 1.15x | 1.23x | +| tsv_wasm-internal | 130.7 | 150.6 | 151.0 | 1.15x | 1.16x | + +## format/css + +| Impl | deno ops/sec | node ops/sec | bun ops/sec | node/deno | bun/deno | +| --- | ---: | ---: | ---: | ---: | ---: | +| prettier | 4.0 | 3.8 | 3.9 | 0.95x | 0.98x | +| tsv | 79.2 | 101.1 | 105.6 | 1.28x | 1.33x | +| tsv_wasm | 66.8 | 77.4 | 78.0 | 1.16x | 1.17x | +| oxfmt | 3.6 | 3.5 | 2.8 | 0.96x | 0.79x | +| biome-wasm | 14.8 | 18.2 | — | 1.23x | — | diff --git a/benches/js/results/report.node.json b/benches/js/results/report.node.json new file mode 100644 index 000000000..6239415c3 --- /dev/null +++ b/benches/js/results/report.node.json @@ -0,0 +1,736 @@ +{ + "version": 5, + "runtime": "node", + "timestamp": "2026-06-26T16:05:52.967Z", + "git_commit": "e4c23f8e", + "corpus": { + "svelte": 1259, + "typescript": 4521, + "css": 182 + }, + "versions": { + "tsv": "0.1.0", + "svelte": "5.56.1", + "acorn": "8.16.0", + "acorn_ts": "1.0.10", + "prettier": "3.8.3", + "prettier_svelte": "4.1.1", + "oxc_parser": "0.134.0", + "oxfmt": "0.53.0", + "biome": "2.4.16" + }, + "binary_sizes": [ + { + "label": "tsv (ffi)", + "bytes": 3717440, + "kind": "native", + "gzip_bytes": 1550897 + }, + { + "label": "tsv format (ffi)", + "bytes": 2935520, + "kind": "native", + "gzip_bytes": 1251205 + }, + { + "label": "tsv parse (ffi)", + "bytes": 2068624, + "kind": "native", + "gzip_bytes": 867280 + }, + { + "label": "tsv (napi)", + "bytes": 3841744, + "kind": "native", + "gzip_bytes": 1591977 + }, + { + "label": "tsv_format_wasm", + "bytes": 2213824, + "kind": "wasm", + "gzip_bytes": 735053 + }, + { + "label": "tsv_parse_wasm", + "bytes": 1565374, + "kind": "wasm", + "gzip_bytes": 492040 + }, + { + "label": "tsv_wasm", + "bytes": 2881851, + "kind": "wasm", + "gzip_bytes": 920193 + }, + { + "label": "biome (wasm)", + "bytes": 34430368, + "kind": "wasm", + "gzip_bytes": 8187702 + }, + { + "label": "oxc-parser (napi)", + "bytes": 2658008, + "kind": "native", + "gzip_bytes": 1032774 + }, + { + "label": "oxfmt (napi)", + "bytes": 8016464, + "kind": "native", + "gzip_bytes": 3224684 + }, + { + "label": "oxc-parser (wasm)", + "bytes": 1866798, + "kind": "wasm", + "gzip_bytes": 518682 + } + ], + "entries": [ + { + "name": "svelte/compiler", + "group": "parse/svelte", + "mean_ns": 460519821.6363636, + "p50_ns": 460422814, + "p75_ns": 463332024, + "p90_ns": 465071237, + "p95_ns": 468095849.5, + "p99_ns": 470515539.5, + "min_ns": 452885544, + "max_ns": 471120462, + "std_dev_ns": 5248688.184362533, + "cv": 0.01139731220626375, + "ops_per_second": 2.171459192454959, + "sample_size": 11, + "files_processed": 1212, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "node" + }, + { + "name": "tsv-json", + "group": "parse/svelte", + "mean_ns": 549611779, + "p50_ns": 549186590.5, + "p75_ns": 554153198.25, + "p90_ns": 561714720.5, + "p95_ns": 562045063.25, + "p99_ns": 562309337.45, + "min_ns": 538345222, + "max_ns": 562375406, + "std_dev_ns": 8379151.02838542, + "cv": 0.01524558124214696, + "ops_per_second": 1.8194660999068581, + "sample_size": 10, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "node" + }, + { + "name": "tsv_wasm-json", + "group": "parse/svelte", + "mean_ns": 542348822.7777778, + "p50_ns": 543610598, + "p75_ns": 544628795.25, + "p90_ns": 547038258, + "p95_ns": 555039433.5, + "p99_ns": 561440373.9, + "min_ns": 537453841, + "max_ns": 563040609, + "std_dev_ns": 2621528.985338793, + "cv": 0.004833658478157957, + "ops_per_second": 1.8438317886968851, + "sample_size": 9, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "node" + }, + { + "name": "tsv-internal", + "group": "parse/svelte", + "mean_ns": 27825114.72067039, + "p50_ns": 27652418.5, + "p75_ns": 28329825.5, + "p90_ns": 28792367.4, + "p95_ns": 29096448.15, + "p99_ns": 29372411.430000003, + "min_ns": 26674830, + "max_ns": 29907294, + "std_dev_ns": 657412.7169118039, + "cv": 0.023626595020772113, + "ops_per_second": 35.93875569027329, + "sample_size": 179, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "node" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/svelte", + "mean_ns": 38555138.608333334, + "p50_ns": 38377422, + "p75_ns": 39482445, + "p90_ns": 41706393.4, + "p95_ns": 43308276.8, + "p99_ns": 46579972.44, + "min_ns": 36633395, + "max_ns": 48316300, + "std_dev_ns": 1317496.0561682512, + "cv": 0.03417173699081156, + "ops_per_second": 25.936879910058455, + "sample_size": 120, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1210, + "runtime": "node" + }, + { + "name": "prettier", + "group": "format/svelte", + "mean_ns": 4416266211.142858, + "p50_ns": 4431401386, + "p75_ns": 4437882394, + "p90_ns": 4450122432, + "p95_ns": 4454561856, + "p99_ns": 4458113395.2, + "min_ns": 4376027750, + "max_ns": 4459001280, + "std_dev_ns": 33108087.900384482, + "cv": 0.007496850578628649, + "ops_per_second": 0.2264356250709842, + "sample_size": 7, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1209, + "runtime": "node" + }, + { + "name": "tsv", + "group": "format/svelte", + "mean_ns": 242260228.05555555, + "p50_ns": 242438319, + "p75_ns": 246252188, + "p90_ns": 251444127, + "p95_ns": 254724943, + "p99_ns": 258228837.4, + "min_ns": 236306398, + "max_ns": 259104811, + "std_dev_ns": 3365180.125239911, + "cv": 0.013890765943092409, + "ops_per_second": 4.127792696416839, + "sample_size": 18, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1209, + "runtime": "node" + }, + { + "name": "tsv_wasm", + "group": "format/svelte", + "mean_ns": 352891975.53333336, + "p50_ns": 352505929, + "p75_ns": 356387978, + "p90_ns": 367242866.8, + "p95_ns": 369188364.09999996, + "p99_ns": 372514488.02, + "min_ns": 333204891, + "max_ns": 373346019, + "std_dev_ns": 10801366.20274057, + "cv": 0.030608137763450782, + "ops_per_second": 2.8337283625922014, + "sample_size": 15, + "files_processed": 1220, + "files_total": 1259, + "files_iterated": 1209, + "runtime": "node" + }, + { + "name": "oxfmt", + "group": "format/svelte", + "mean_ns": 4346654087.857142, + "p50_ns": 4329093407, + "p75_ns": 4374756750, + "p90_ns": 4392431088.6, + "p95_ns": 4397181613.8, + "p99_ns": 4400982033.96, + "min_ns": 4310719296, + "max_ns": 4401932139, + "std_dev_ns": 37121981.576470226, + "cv": 0.00854035789969452, + "ops_per_second": 0.23006201546923422, + "sample_size": 7, + "files_processed": 1217, + "files_total": 1259, + "files_iterated": 1209, + "runtime": "node" + }, + { + "name": "biome-wasm", + "group": "format/svelte", + "mean_ns": 696247855.625, + "p50_ns": 696021053.5, + "p75_ns": 698726481.5, + "p90_ns": 700920667.4, + "p95_ns": 701746768.2, + "p99_ns": 702407648.84, + "min_ns": 691423120, + "max_ns": 702572869, + "std_dev_ns": 4053900.7516915267, + "cv": 0.005822496570639297, + "ops_per_second": 1.4362701327134877, + "sample_size": 8, + "files_processed": 1257, + "files_total": 1259, + "files_iterated": 1209, + "runtime": "node" + }, + { + "name": "acorn-typescript", + "group": "parse/typescript", + "mean_ns": 3076358260.8, + "p50_ns": 3081831315, + "p75_ns": 3085922639, + "p90_ns": 3087326909.6, + "p95_ns": 3087794999.8, + "p99_ns": 3088169471.96, + "min_ns": 3058750622, + "max_ns": 3088263090, + "std_dev_ns": 12848353.375428025, + "cv": 0.004176481503843716, + "ops_per_second": 0.32505966965627475, + "sample_size": 5, + "files_processed": 4189, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "node" + }, + { + "name": "tsv-json", + "group": "parse/typescript", + "mean_ns": 2700459663.8, + "p50_ns": 2693962807, + "p75_ns": 2717046507, + "p90_ns": 2724142026, + "p95_ns": 2726507199, + "p99_ns": 2728399337.4, + "min_ns": 2680058864, + "max_ns": 2728872372, + "std_dev_ns": 21612992.219434742, + "cv": 0.008003449379066685, + "ops_per_second": 0.3703073270840239, + "sample_size": 5, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "node" + }, + { + "name": "tsv_wasm-json", + "group": "parse/typescript", + "mean_ns": 2920718620.5, + "p50_ns": 2915295577, + "p75_ns": 2921635900, + "p90_ns": 2929537027.6, + "p95_ns": 2932170736.8, + "p99_ns": 2934277704.16, + "min_ns": 2911138559, + "max_ns": 2934804446, + "std_dev_ns": 10335035.249806762, + "cv": 0.003538524792243595, + "ops_per_second": 0.3423814923427335, + "sample_size": 4, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "node" + }, + { + "name": "tsv-internal", + "group": "parse/typescript", + "mean_ns": 207661643.6, + "p50_ns": 207910263, + "p75_ns": 208788219, + "p90_ns": 209654355.8, + "p95_ns": 209901368.6, + "p99_ns": 210464396, + "min_ns": 204851227, + "max_ns": 210633548, + "std_dev_ns": 1627416.2355404045, + "cv": 0.007836864850569855, + "ops_per_second": 4.815525788316548, + "sample_size": 25, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "node" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/typescript", + "mean_ns": 264986878.31578946, + "p50_ns": 264872314, + "p75_ns": 265371800.5, + "p90_ns": 266146419.4, + "p95_ns": 266467473, + "p99_ns": 266669965.8, + "min_ns": 263622989, + "max_ns": 266720589, + "std_dev_ns": 868840.6711757439, + "cv": 0.0032788063948597916, + "ops_per_second": 3.7737717669487116, + "sample_size": 19, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "node" + }, + { + "name": "oxc-parser", + "group": "parse/typescript", + "mean_ns": 1285272706.6, + "p50_ns": 1284306062, + "p75_ns": 1290462919, + "p90_ns": 1298897336.8, + "p95_ns": 1301708809.4, + "p99_ns": 1303957987.48, + "min_ns": 1271248715, + "max_ns": 1304520282, + "std_dev_ns": 13079209.447332447, + "cv": 0.010176213483854002, + "ops_per_second": 0.7780449976607323, + "sample_size": 5, + "files_processed": 4329, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "node" + }, + { + "name": "oxc-parser-wasm", + "group": "parse/typescript", + "mean_ns": 1337305811, + "p50_ns": 1333881007, + "p75_ns": 1337935981, + "p90_ns": 1345591239.4, + "p95_ns": 1348142992.2, + "p99_ns": 1350184394.44, + "min_ns": 1330452412, + "max_ns": 1350694745, + "std_dev_ns": 7942817.013954716, + "cv": 0.005939417109101843, + "ops_per_second": 0.747772118968232, + "sample_size": 5, + "files_processed": 4521, + "files_total": 4521, + "files_iterated": 4120, + "runtime": "node" + }, + { + "name": "prettier", + "group": "format/typescript", + "mean_ns": 13530175349.571428, + "p50_ns": 13565356847, + "p75_ns": 13608654437, + "p90_ns": 13685762982.8, + "p95_ns": 13718048651.9, + "p99_ns": 13743877187.18, + "min_ns": 13337759540, + "max_ns": 13750334321, + "std_dev_ns": 144670076.113311, + "cv": 0.010692402158549516, + "ops_per_second": 0.07390887214419399, + "sample_size": 7, + "files_processed": 4351, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "node" + }, + { + "name": "tsv", + "group": "format/typescript", + "mean_ns": 828181230.5, + "p50_ns": 829474298, + "p75_ns": 830763649, + "p90_ns": 841818961.6, + "p95_ns": 850035221.8, + "p99_ns": 856608229.96, + "min_ns": 824390444, + "max_ns": 858251482, + "std_dev_ns": 2596616.5472364803, + "cv": 0.003135324071120059, + "ops_per_second": 1.2074651817407978, + "sample_size": 6, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "node" + }, + { + "name": "tsv_wasm", + "group": "format/typescript", + "mean_ns": 1049490559.6, + "p50_ns": 1048186918, + "p75_ns": 1051124496, + "p90_ns": 1053196050, + "p95_ns": 1053886568, + "p99_ns": 1054438982.4, + "min_ns": 1046485510, + "max_ns": 1054577086, + "std_dev_ns": 3357415.1421673787, + "cv": 0.0031990903695665587, + "ops_per_second": 0.9528432541414544, + "sample_size": 5, + "files_processed": 4260, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "node" + }, + { + "name": "oxfmt", + "group": "format/typescript", + "mean_ns": 1043309450.6, + "p50_ns": 1038106859, + "p75_ns": 1048998909, + "p90_ns": 1061121142.2, + "p95_ns": 1065161886.6, + "p99_ns": 1068394482.12, + "min_ns": 1028403158, + "max_ns": 1069202631, + "std_dev_ns": 16460090.3032646, + "cv": 0.015776805523805535, + "ops_per_second": 0.9584883942390313, + "sample_size": 5, + "files_processed": 4332, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "node" + }, + { + "name": "biome-wasm", + "group": "format/typescript", + "mean_ns": 4227132372.2, + "p50_ns": 4226520582, + "p75_ns": 4229707731, + "p90_ns": 4237808559.6, + "p95_ns": 4240508835.8, + "p99_ns": 4242669056.76, + "min_ns": 4214235300, + "max_ns": 4243209112, + "std_dev_ns": 10703453.563400237, + "cv": 0.0025320838386306914, + "ops_per_second": 0.23656699434741207, + "sample_size": 5, + "files_processed": 4521, + "files_total": 4521, + "files_iterated": 4219, + "runtime": "node" + }, + { + "name": "svelte/compiler", + "group": "parse/css", + "mean_ns": 5674981.744769874, + "p50_ns": 5675173, + "p75_ns": 5779161, + "p90_ns": 6076480.6, + "p95_ns": 6176071.8, + "p99_ns": 6446288.039999994, + "min_ns": 5445454, + "max_ns": 6906669, + "std_dev_ns": 66349.12347821353, + "cv": 0.011691513111801922, + "ops_per_second": 176.21202057990953, + "sample_size": 717, + "files_processed": 152, + "files_total": 182, + "files_iterated": 148, + "runtime": "node" + }, + { + "name": "tsv-json", + "group": "parse/css", + "mean_ns": 12977907.827195467, + "p50_ns": 12904785.5, + "p75_ns": 13270227.75, + "p90_ns": 13843168.6, + "p95_ns": 14260476.749999998, + "p99_ns": 15100004.379999999, + "min_ns": 12545202, + "max_ns": 16392338, + "std_dev_ns": 332659.4852305573, + "cv": 0.025632751415714528, + "ops_per_second": 77.05402236749438, + "sample_size": 353, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "node" + }, + { + "name": "tsv_wasm-json", + "group": "parse/css", + "mean_ns": 15161721.315960912, + "p50_ns": 15133874.5, + "p75_ns": 15355264.75, + "p90_ns": 15682443.5, + "p95_ns": 16132697.15, + "p99_ns": 16849124.020000003, + "min_ns": 14800725, + "max_ns": 22947633, + "std_dev_ns": 234543.9985316129, + "cv": 0.015469483552946315, + "ops_per_second": 65.9555718747639, + "sample_size": 307, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "node" + }, + { + "name": "tsv-internal", + "group": "parse/css", + "mean_ns": 4823174.063740857, + "p50_ns": 4814582, + "p75_ns": 4863945, + "p90_ns": 4942298, + "p95_ns": 5028450.2, + "p99_ns": 5181341.199999999, + "min_ns": 4741773, + "max_ns": 5438369, + "std_dev_ns": 49410.0654781549, + "cv": 0.010244304855096278, + "ops_per_second": 207.33234728509868, + "sample_size": 957, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "node" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/css", + "mean_ns": 6639633.008784773, + "p50_ns": 6631641, + "p75_ns": 6687427.75, + "p90_ns": 6811301.2, + "p95_ns": 6917467.15, + "p99_ns": 7045058.15, + "min_ns": 6551900, + "max_ns": 7555351, + "std_dev_ns": 56256.47336614011, + "cv": 0.008472828737929977, + "ops_per_second": 150.61073385786818, + "sample_size": 683, + "files_processed": 154, + "files_total": 182, + "files_iterated": 148, + "runtime": "node" + }, + { + "name": "prettier", + "group": "format/css", + "mean_ns": 263628109.83333334, + "p50_ns": 263304673, + "p75_ns": 267583880, + "p90_ns": 270831078.2, + "p95_ns": 275674468.6999999, + "p99_ns": 297184221.74, + "min_ns": 256524612, + "max_ns": 302561660, + "std_dev_ns": 4691145.066799993, + "cv": 0.017794555632803164, + "ops_per_second": 3.793222204689036, + "sample_size": 18, + "files_processed": 181, + "files_total": 182, + "files_iterated": 154, + "runtime": "node" + }, + { + "name": "tsv", + "group": "format/css", + "mean_ns": 9894566.340425532, + "p50_ns": 9878792, + "p75_ns": 10043937, + "p90_ns": 10292521, + "p95_ns": 10454168, + "p99_ns": 11052136, + "min_ns": 9781647, + "max_ns": 11677218, + "std_dev_ns": 95076.69653038164, + "cv": 0.009608980652535876, + "ops_per_second": 101.06557130395603, + "sample_size": 423, + "files_processed": 154, + "files_total": 182, + "files_iterated": 154, + "runtime": "node" + }, + { + "name": "tsv_wasm", + "group": "format/css", + "mean_ns": 12926065.221590908, + "p50_ns": 12919280, + "p75_ns": 12969386, + "p90_ns": 13072597.4, + "p95_ns": 13263006.2, + "p99_ns": 13874546.119999984, + "min_ns": 12851673, + "max_ns": 21923616, + "std_dev_ns": 49833.05694937264, + "cv": 0.0038552379316587814, + "ops_per_second": 77.3630631485335, + "sample_size": 352, + "files_processed": 154, + "files_total": 182, + "files_iterated": 154, + "runtime": "node" + }, + { + "name": "oxfmt", + "group": "format/css", + "mean_ns": 288994107.4444444, + "p50_ns": 289283819.5, + "p75_ns": 294131086.75, + "p90_ns": 296024037.1, + "p95_ns": 298473210.25, + "p99_ns": 302334464.45, + "min_ns": 278485858, + "max_ns": 303299778, + "std_dev_ns": 7004351.959271854, + "cv": 0.024237006149401694, + "ops_per_second": 3.4602781656793393, + "sample_size": 18, + "files_processed": 181, + "files_total": 182, + "files_iterated": 154, + "runtime": "node" + }, + { + "name": "biome-wasm", + "group": "format/css", + "mean_ns": 54798579.31111111, + "p50_ns": 54753750, + "p75_ns": 55061631.5, + "p90_ns": 55378274.2, + "p95_ns": 55661000.15, + "p99_ns": 56701458.46, + "min_ns": 54211043, + "max_ns": 57160184, + "std_dev_ns": 382798.5097647677, + "cv": 0.0069855553661616636, + "ops_per_second": 18.248648278318363, + "sample_size": 90, + "files_processed": 182, + "files_total": 182, + "files_iterated": 154, + "runtime": "node" + } + ], + "suppressed_noise": { + "oxfmt::textToDoc": 1, + "panicked at crates/biome_rowan": 1 + } +} \ No newline at end of file diff --git a/benches/js/results/report.node.md b/benches/js/results/report.node.md new file mode 100644 index 000000000..055bcc4f4 --- /dev/null +++ b/benches/js/results/report.node.md @@ -0,0 +1,197 @@ +# tsv benchmark results + +**Runtime:** node + +**Date:** 2026-06-26T16:05:52.967Z — tsv 0.1.0 (e4c23f8e) + +**Corpus:** 1259 Svelte (1.8 MB), 4521 TypeScript (16.2 MB), 182 CSS (0.2 MB) — 5962 files, 18.2 MB total + +**Versions:** svelte@5.56.1, acorn@8.16.0, acorn-typescript@1.0.10, prettier@3.8.3, prettier-plugin-svelte@4.1.1, oxc-parser@0.134.0, oxfmt@0.53.0, @biomejs/wasm-bundler@2.4.16 + +**Methodology:** Single-threaded — every implementation formats/parses one file at a time, measured sequentially with no cross-file parallelism. The numbers are per-file, single-core latency/throughput, not the multi-core batch throughput a CLI gets formatting many files at once. + +## parse/svelte + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs svelte/compiler (speedup) | +| ----------------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------------------------- | +| svelte/compiler | 2.17 | 11 | 460.42 | 463.33 | 465.07 | 468.10 | 470.52 | 452.89 | 471.12 | baseline | +| tsv-json | 1.82 | 10 | 549.19 | 554.15 | 561.71 | 562.05 | 562.31 | 538.35 | 562.38 | 0.84x | +| tsv_wasm-json | 1.84 | 9 | 543.61 | 544.63 | 547.04 | — | — | 537.45 | 563.04 | 0.85x | +| tsv-internal | 35.94 | 179 | 27.65 | 28.33 | 28.79 | 29.10 | 29.37 | 26.67 | 29.91 | 16.6x | +| tsv_wasm-internal | 25.94 | 120 | 38.38 | 39.48 | 41.71 | 43.31 | 46.58 | 36.63 | 48.32 | 11.9x | + +**Files (intersection):** 1210 + +**Throughput:** svelte/compiler 3.8 MB/s, tsv-json 3.2 MB/s, tsv_wasm-json 3.2 MB/s, tsv-internal 62.9 MB/s, tsv_wasm-internal 45.4 MB/s + +**Coverage:** svelte/compiler 1212/1259 (96%), tsv-json 1220/1259 (96%), tsv_wasm-json 1220/1259 (96%), tsv-internal 1220/1259 (96%), tsv_wasm-internal 1220/1259 (96%) + +**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 19.8x tsv-internal, tsv_wasm-json 14.1x tsv_wasm-internal + +## format/svelte + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs prettier (speedup) | +| ---------- | ------- | -- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | --------------------- | +| prettier | 0.23 | 7 | 4431.40 | 4437.88 | 4450.12 | — | — | 4376.03 | 4459.00 | baseline | +| tsv | 4.13 | 18 | 242.44 | 246.25 | 251.44 | 254.72 | 258.23 | 236.31 | 259.10 | 18.2x | +| tsv_wasm | 2.83 | 15 | 352.51 | 356.39 | 367.24 | 369.19 | 372.51 | 333.20 | 373.35 | 12.5x | +| oxfmt | 0.23 | 7 | 4329.09 | 4374.76 | 4392.43 | — | — | 4310.72 | 4401.93 | 1.02x | +| biome-wasm | 1.44 | 8 | 696.02 | 698.73 | 700.92 | — | — | 691.42 | 702.57 | 6.34x | + +**Files (intersection):** 1209 + +**Throughput:** prettier 0.4 MB/s, tsv 7.0 MB/s, tsv_wasm 4.8 MB/s, oxfmt 0.4 MB/s, biome-wasm 2.4 MB/s + +**Coverage:** prettier 1220/1259 (96%), tsv 1220/1259 (96%), tsv_wasm 1220/1259 (96%), oxfmt 1217/1259 (96%), biome-wasm 1257/1259 (99%) + +## parse/typescript + +| Task Name | ops/sec | n | p50 (s) | p75 (s) | p90 (s) | p95 (s) | p99 (s) | min (s) | max (s) | vs acorn-typescript (speedup) | +| ----------------- | ------- | -- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ----------------------------- | +| acorn-typescript | 0.33 | 5 | 3.08 | 3.09 | 3.09 | — | — | 3.06 | 3.09 | baseline | +| tsv-json | 0.37 | 5 | 2.69 | 2.72 | 2.72 | — | — | 2.68 | 2.73 | 1.14x | +| tsv_wasm-json | 0.34 | 4 | 2.92 | 2.92 | 2.93 | — | — | 2.91 | 2.93 | 1.05x | +| tsv-internal | 4.82 | 25 | 0.21 | 0.21 | 0.21 | 0.21 | 0.21 | 0.20 | 0.21 | 14.8x | +| tsv_wasm-internal | 3.77 | 19 | 0.26 | 0.27 | 0.27 | 0.27 | 0.27 | 0.26 | 0.27 | 11.6x | +| oxc-parser | 0.78 | 5 | 1.28 | 1.29 | 1.30 | — | — | 1.27 | 1.30 | 2.39x | +| oxc-parser-wasm | 0.75 | 5 | 1.33 | 1.34 | 1.35 | — | — | 1.33 | 1.35 | 2.30x | + +**Files (intersection):** 4120 + +**Throughput:** acorn-typescript 5.2 MB/s, tsv-json 5.9 MB/s, tsv_wasm-json 5.5 MB/s, tsv-internal 77.1 MB/s, tsv_wasm-internal 60.4 MB/s, oxc-parser 12.5 MB/s, oxc-parser-wasm 12.0 MB/s + +**Coverage:** acorn-typescript 4189/4521 (92%), tsv-json 4260/4521 (94%), tsv_wasm-json 4260/4521 (94%), tsv-internal 4260/4521 (94%), tsv_wasm-internal 4260/4521 (94%), oxc-parser 4329/4521 (95%), oxc-parser-wasm 4521/4521 (100%) + +**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 13.0x tsv-internal, tsv_wasm-json 11.0x tsv_wasm-internal + +## format/typescript + +| Task Name | ops/sec | n | p50 (s) | p75 (s) | p90 (s) | p95 (s) | p99 (s) | min (s) | max (s) | vs prettier (speedup) | +| ---------- | ------- | - | ------- | ------- | ------- | ------- | ------- | ------- | ------- | --------------------- | +| prettier | 0.07 | 7 | 13.57 | 13.61 | 13.69 | — | — | 13.34 | 13.75 | baseline | +| tsv | 1.21 | 6 | 0.83 | 0.83 | 0.84 | — | — | 0.82 | 0.86 | 16.3x | +| tsv_wasm | 0.95 | 5 | 1.05 | 1.05 | 1.05 | — | — | 1.05 | 1.05 | 12.9x | +| oxfmt | 0.96 | 5 | 1.04 | 1.05 | 1.06 | — | — | 1.03 | 1.07 | 13.0x | +| biome-wasm | 0.24 | 5 | 4.23 | 4.23 | 4.24 | — | — | 4.21 | 4.24 | 3.20x | + +**Files (intersection):** 4219 + +**Throughput:** prettier 1.2 MB/s, tsv 19.4 MB/s, tsv_wasm 15.3 MB/s, oxfmt 15.4 MB/s, biome-wasm 3.8 MB/s + +**Coverage:** prettier 4351/4521 (96%), tsv 4260/4521 (94%), tsv_wasm 4260/4521 (94%), oxfmt 4332/4521 (95%), biome-wasm 4521/4521 (100%) + +## parse/css + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs svelte/compiler (speedup) | +| ----------------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------------------------- | +| svelte/compiler | 176.21 | 717 | 5.68 | 5.78 | 6.08 | 6.18 | 6.45 | 5.45 | 6.91 | baseline | +| tsv-json | 77.05 | 353 | 12.90 | 13.27 | 13.84 | 14.26 | 15.10 | 12.55 | 16.39 | 0.44x | +| tsv_wasm-json | 65.96 | 307 | 15.13 | 15.36 | 15.68 | 16.13 | 16.85 | 14.80 | 22.95 | 0.37x | +| tsv-internal | 207.33 | 957 | 4.81 | 4.86 | 4.94 | 5.03 | 5.18 | 4.74 | 5.44 | 1.18x | +| tsv_wasm-internal | 150.61 | 683 | 6.63 | 6.69 | 6.81 | 6.92 | 7.05 | 6.55 | 7.56 | 0.85x | + +**Files (intersection):** 148 + +**Throughput:** svelte/compiler 32.2 MB/s, tsv-json 14.1 MB/s, tsv_wasm-json 12.0 MB/s, tsv-internal 37.8 MB/s, tsv_wasm-internal 27.5 MB/s + +**Coverage:** svelte/compiler 152/182 (83%), tsv-json 154/182 (84%), tsv_wasm-json 154/182 (84%), tsv-internal 154/182 (84%), tsv_wasm-internal 154/182 (84%) + +**JSON overhead** (json_ns / internal_ns, higher = more cost): tsv-json 2.7x tsv-internal, tsv_wasm-json 2.3x tsv_wasm-internal + +## format/css + +| Task Name | ops/sec | n | p50 (ms) | p75 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | min (ms) | max (ms) | vs prettier (speedup) | +| ---------- | ------- | --- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | --------------------- | +| prettier | 3.79 | 18 | 263.30 | 267.58 | 270.83 | 275.67 | 297.18 | 256.52 | 302.56 | baseline | +| tsv | 101.07 | 423 | 9.88 | 10.04 | 10.29 | 10.45 | 11.05 | 9.78 | 11.68 | 26.6x | +| tsv_wasm | 77.36 | 352 | 12.92 | 12.97 | 13.07 | 13.26 | 13.87 | 12.85 | 21.92 | 20.4x | +| oxfmt | 3.46 | 18 | 289.28 | 294.13 | 296.02 | 298.47 | 302.33 | 278.49 | 303.30 | 0.91x | +| biome-wasm | 18.25 | 90 | 54.75 | 55.06 | 55.38 | 55.66 | 56.70 | 54.21 | 57.16 | 4.81x | + +**Files (intersection):** 154 + +**Throughput:** prettier 0.7 MB/s, tsv 19.5 MB/s, tsv_wasm 14.9 MB/s, oxfmt 0.7 MB/s, biome-wasm 3.5 MB/s + +**Coverage:** prettier 181/182 (99%), tsv 154/182 (84%), tsv_wasm 154/182 (84%), oxfmt 181/182 (99%), biome-wasm 182/182 (100%) + +_Note: every `Nx` is speedup form — values > 1 mean self is faster. File counts come from the per-group `Files (intersection):` / `Coverage:` lines and the Comparisons table row labels._ + +## Binary Sizes + +| Binary | Size | Gzipped | vs tsv | vs tsv (gz) | +| --- | ---: | ---: | ---: | ---: | +| tsv_format_wasm | 2.2 MB | 735.1 KB | 0.8x | 0.8x | +| tsv_parse_wasm | 1.6 MB | 492.0 KB | 0.5x | 0.5x | +| tsv_wasm | 2.9 MB | 920.2 KB | — | — | +| biome (wasm) | 34.4 MB | 8.2 MB | 11.9x | 8.9x | +| oxc-parser (wasm) | 1.9 MB | 518.7 KB | 0.6x | 0.6x | +| tsv (ffi) | 3.7 MB | 1.6 MB | 1.0x | 1.0x | +| tsv format (ffi) | 2.9 MB | 1.3 MB | 0.8x | 0.8x | +| tsv parse (ffi) | 2.1 MB | 867.3 KB | 0.5x | 0.5x | +| tsv (napi) | 3.8 MB | 1.6 MB | — | — | +| oxc-parser+oxfmt (napi) | 10.7 MB | 4.3 MB | 2.8x | 2.7x | +| oxc-parser (napi) | 2.7 MB | 1.0 MB | 0.7x | 0.6x | +| oxfmt (napi) | 8.0 MB | 3.2 MB | 2.1x | 2.0x | + +_Gzipped ≈ npm-tarball wire size (`gzip -c`, system default level). `vs tsv (gz)` compares gzipped bytes; `vs tsv` compares raw on-disk bytes._ + +## Comparisons to tsv (speedup) + +| Benchmark | Comparisons | +| --- | --- | +| format svelte (1209f) | **18.2x** prettier, **17.9x** oxfmt | +| format typescript (4219f) | **16.3x** prettier, **1.26x** oxfmt | +| format css (154f) | **26.6x** prettier, **29.2x** oxfmt | +| parse svelte (1210f) | **0.84x** svelte | +| parse typescript (4120f) | **1.14x** svelte, **0.48x** oxc-parser | +| parse css (148f) | **0.44x** svelte | + +## Comparisons to tsv_wasm (speedup) + +| Benchmark | Comparisons | +| --- | --- | +| format svelte (1209f) | **12.5x** prettier, **1.97x** biome-wasm | +| format typescript (4219f) | **12.9x** prettier, **4.03x** biome-wasm | +| format css (154f) | **20.4x** prettier, **4.24x** biome-wasm | +| parse svelte (1210f) | **0.85x** svelte | +| parse typescript (4120f) | **1.05x** svelte, **0.46x** oxc-parser-wasm | +| parse css (148f) | **0.37x** svelte | + +_`Nx` is speedup — self is N× faster than the named opponent. `(Mf)` is the self impl's iterated count (per-group intersection in default mode; per-impl success set in `BENCH_MODE=union`). Parse canonical: svelte/compiler for .svelte/.css, acorn-typescript for .ts. oxc-parser (native and wasm) serializes the AST to JSON in Rust and deserializes it in JS — the same eager materialization as tsv-json/tsv_wasm-json, so these parse rows are apples-to-apples. tsv-internal/tsv_wasm-internal are parse-only (no JS materialization) and have no oxc counterpart — oxc exposes no comparably cheap mode (its JS API always serializes; experimentalLazy is setup-dominated). Format groups include parse time — each formatter parses internally._ + +## Skipped Files + +1373 unique file+error combinations — Svelte 169, TypeScript 1144, CSS 60. + +**Per-benchmark skip counts:** +- parse/typescript: acorn-typescript: 332 +- parse/typescript: tsv-json: 261 +- parse/typescript: tsv_wasm-json: 261 +- parse/typescript: tsv-internal: 261 +- parse/typescript: tsv_wasm-internal: 261 +- format/typescript: tsv: 261 +- format/typescript: tsv_wasm: 261 +- parse/typescript: oxc-parser: 192 +- format/typescript: oxfmt: 189 +- format/typescript: prettier: 170 +- parse/svelte: svelte/compiler: 47 +- format/svelte: oxfmt: 42 +- parse/svelte: tsv-json: 39 +- parse/svelte: tsv_wasm-json: 39 +- parse/svelte: tsv-internal: 39 +- parse/svelte: tsv_wasm-internal: 39 +- format/svelte: prettier: 39 +- format/svelte: tsv: 39 +- format/svelte: tsv_wasm: 39 +- parse/css: svelte/compiler: 30 +- parse/css: tsv-json: 28 +- parse/css: tsv_wasm-json: 28 +- parse/css: tsv-internal: 28 +- parse/css: tsv_wasm-internal: 28 +- format/css: tsv: 28 +- format/css: tsv_wasm: 28 +- format/svelte: biome-wasm: 2 +- format/css: prettier: 1 +- format/css: oxfmt: 1 + +_Per-file detail omitted. Re-run with `--verbose` to include error messages and failure sets per file._ diff --git a/benches/deno/smoke.ts b/benches/js/smoke.ts similarity index 77% rename from benches/deno/smoke.ts rename to benches/js/smoke.ts index 4f7c1d9c1..6802b8475 100644 --- a/benches/deno/smoke.ts +++ b/benches/js/smoke.ts @@ -10,13 +10,18 @@ * Exit codes: 0 = all pass, 1 = any failure. */ +import { stat } from 'node:fs/promises'; +import { env, exit } from 'node:process'; +import { fileURLToPath } from 'node:url'; import { check_artifact_freshness, wasm_artifact_path } from './lib/check_artifact_freshness.ts'; import { get_library_path } from './lib/ffi.ts'; +import { get_napi_library_path } from './lib/napi.ts'; import { get_benchmark_tasks, get_formatters, init_implementations, } from './lib/implementations.ts'; +import { current_runtime } from './lib/runtime.ts'; import { type Language, LANGUAGES } from './lib/types.ts'; /** @@ -52,24 +57,48 @@ function record_fail(f: Failure): void { failures.push(f); } -// Refuse to smoke stale binaries (smoke skips the rebuild for speed, same as -// the bench/corpus :run tasks). See lib/check_artifact_freshness.ts; override -// with BENCH_STALE_OK=1. -await check_artifact_freshness([ - { - label: 'FFI (release)', +// Refuse to smoke stale binaries (smoke skips the rebuild for speed, same as the +// bench/corpus :run tasks). See lib/check_artifact_freshness.ts; override with +// BENCH_STALE_OK=1. The native + WASM artifacts are runtime-specific: Deno runs +// the FFI library + the `deno`-target WASM bundle; Node/Bun run the N-API addon + +// the `nodejs` target (`wasm_artifact_path` resolves the runtime's own bundle). +const wasm_target = current_runtime() === 'deno' ? 'deno' : 'nodejs'; +const native_check = current_runtime() === 'deno' + ? { + label: `FFI (${env.TSV_FFI_PROFILE ?? 'release'})`, path: get_library_path(), binding_crates: ['tsv_ffi'], rebuild: 'deno task build:ffi', - }, + } + : { + label: 'N-API', + path: get_napi_library_path(), + binding_crates: ['tsv_napi'], + rebuild: 'deno task build:napi', + }; +await check_artifact_freshness([ + native_check, { - label: 'WASM (all/deno)', + label: `WASM (all/${wasm_target})`, path: wasm_artifact_path('all'), binding_crates: ['tsv_wasm'], - rebuild: 'deno task build:wasm:all:deno', + rebuild: `deno task build:wasm:all:${wasm_target}`, }, ]); +// Friendly preflight: the canonical impls (prettier + svelte/compiler) resolve +// from the harness node_modules; without it, init fails opaquely. (Cheap check.) +const node_modules_path = fileURLToPath(new URL('./node_modules', import.meta.url)); +try { + await stat(node_modules_path); +} catch { + console.error( + `\n✗ Harness dependencies not installed (${node_modules_path} missing).\n` + + ` Run 'deno task bench:install' first.\n`, + ); + exit(1); +} + const impls = await init_implementations({ logger: () => {} }); // @@ -177,11 +206,11 @@ for (const lang of LANGUAGES) { console.log(); if (failures.length === 0) { console.log(`All ${passed} checks passed.`); - Deno.exit(0); + exit(0); } else { console.log(`${failures.length} failure(s), ${passed} passed:`); for (const f of failures) { console.log(` ${f.kind}/${f.lang}/${f.impl}: ${f.reason}`); } - Deno.exit(1); + exit(1); } diff --git a/crates/tsv_debug/CLAUDE.md b/crates/tsv_debug/CLAUDE.md index f23b046ae..723697f96 100644 --- a/crates/tsv_debug/CLAUDE.md +++ b/crates/tsv_debug/CLAUDE.md @@ -14,7 +14,7 @@ The Deno sidecar is a **long-running subprocess pool** spawned lazily on first u - `deno/` — Sidecar plumbing: `actor.rs` owns one spawned process (`mod.rs` holds the pool + round-robin dispatch), `protocol.rs` is the JSON-lines wire format, `sidecar.ts` is the embedded TypeScript that runs inside Deno and dispatches to prettier / svelte / acorn / parseCss. Pinned npm versions live in `sidecar.ts`. - `fixtures/` — The fixture workflow surface — `model.rs` holds the data model (`InputType`, `Fixture`, divergence-suffix rules), `discovery.rs` walks the fixtures tree, `variants.rs` discovers variant files, `mod.rs` keeps IO/parse helpers, `validation/` is the validator (`fixtures_validate`: structure rules, per-phase checks, errors, summary), `audit_signature.rs` pins prettier's multi-pass chain. -- `test262/` — ECMAScript conformance runner: `discovery.rs` walks the test262 tree, `frontmatter.rs` parses the YAML harness metadata, `runner.rs` drives our parser (`run_test`) and grades the strict subset into the differential `Manifest` (`grade_for_manifest`, sharing the `classify` skip logic with `run_test`). Pure Rust — no Deno needed. The `test262 --emit-manifest` JSON feeds `benches/deno/diagnostics/test262_compare.ts` for the tsv-vs-oxc-parser comparison. +- `test262/` — ECMAScript conformance runner: `discovery.rs` walks the test262 tree, `frontmatter.rs` parses the YAML harness metadata, `runner.rs` drives our parser (`run_test`) and grades the strict subset into the differential `Manifest` (`grade_for_manifest`, sharing the `classify` skip logic with `run_test`). Pure Rust — no Deno needed. The `test262 --emit-manifest` JSON feeds `benches/js/diagnostics/test262_compare.ts` for the tsv-vs-oxc-parser comparison. - `diff.rs` — Colored text / JSON diffing with line-width annotations (used by `compare`, `ast_diff`, validation failures). - `error.rs` — `DebugError` — wraps `DenoError`, `io::Error`, `serde_json::Error`; exposes `hint()` for actionable messages. - `cli/` — argh `TopLevel`/`Subcommand` dispatch in `mod.rs` + per-command `FromArgs` modules under `cli/commands/`. Shared three-mode input plumbing (file path / `--content` / `--stdin`) comes from `tsv_cli::cli::input::InputArgs`. diff --git a/crates/tsv_debug/src/cli/commands/json_profile.rs b/crates/tsv_debug/src/cli/commands/json_profile.rs index 2c2534e44..ce32a3349 100644 --- a/crates/tsv_debug/src/cli/commands/json_profile.rs +++ b/crates/tsv_debug/src/cli/commands/json_profile.rs @@ -55,7 +55,7 @@ use tsv_lang::{ByteToCharMap, LocationTracker}; use super::profile::{format_duration, format_size, lang_label, median_us, resolve_profile_files}; use crate::cli::CliError; -/// Bench-corpus exclusions, mirrored from `benches/deno/lib/corpus.ts`: +/// Bench-corpus exclusions, mirrored from `benches/js/lib/corpus.ts`: /// declaration files and build output add noise without exercising new /// code paths. fn is_bench_corpus_excluded(path: &Path) -> bool { diff --git a/crates/tsv_debug/src/cli/commands/test262.rs b/crates/tsv_debug/src/cli/commands/test262.rs index 9923aad19..d865bfd6c 100644 --- a/crates/tsv_debug/src/cli/commands/test262.rs +++ b/crates/tsv_debug/src/cli/commands/test262.rs @@ -36,7 +36,7 @@ pub struct Test262Command { /// emit a JSON manifest of the graded strict subset (relative path, module /// flag, expected verdict, tsv verdict) to this file and exit — the input - /// to `benches/deno/diagnostics/test262_compare.ts` (tsv vs oxc-parser) + /// to `benches/js/diagnostics/test262_compare.ts` (tsv vs oxc-parser) #[argh(option)] emit_manifest: Option, diff --git a/crates/tsv_debug/src/deno/sidecar.ts b/crates/tsv_debug/src/deno/sidecar.ts index 8b3a4037c..9fe44ea78 100644 --- a/crates/tsv_debug/src/deno/sidecar.ts +++ b/crates/tsv_debug/src/deno/sidecar.ts @@ -4,14 +4,14 @@ // ⚠ SYNC — each canonical version below is pinned in THREE places that must stay // identical: (1) this VERSIONS object, (2) the static `import` specifiers just below // (a static import can't interpolate a const, so the literal is repeated by necessity), -// and (3) the benches/deno/deno.json import map (read by benches/deno/lib/versions.ts). +// and (3) benches/js/package.json (read by benches/js/lib/versions.ts). // These can't be DRYed: the release binary embeds this file as a string and runs it -// WITHOUT deno.json, so it can't read the import map at runtime. +// WITHOUT that package.json, so it can't read the bench's pins at runtime. // // Bumping prettier / svelte / acorn / @sveltejs/acorn-typescript / prettier-plugin-svelte // is NOT a routine refresh — it re-baselines the entire fixture corpus (these tools define // every fixture's expected.json + output_prettier.*). After any bump: run -// `deno task fixtures:update` and review the churn. See benches/deno/CLAUDE.md +// `deno task fixtures:update` and review the churn. See benches/js/CLAUDE.md // §"Canonical baseline is coupled" for the full procedure. // // NOTE: Requires deno.json with "acorn": "npm:acorn@8.16.0" import map diff --git a/crates/tsv_debug/src/test262/runner.rs b/crates/tsv_debug/src/test262/runner.rs index eca10ff4c..3be00b709 100644 --- a/crates/tsv_debug/src/test262/runner.rs +++ b/crates/tsv_debug/src/test262/runner.rs @@ -189,7 +189,7 @@ pub enum Verdict { /// /// `expected` is what test262 wants (accept for positives, reject for /// parse-phase negatives); `tsv` is what `tsv_ts::parse` actually did. A -/// downstream consumer (`benches/deno/diagnostics/test262_compare.ts`) runs the +/// downstream consumer (`benches/js/diagnostics/test262_compare.ts`) runs the /// alternative parser over the same file and joins on `relative_path`. #[derive(Debug, serde::Serialize)] pub struct ManifestEntry { diff --git a/crates/tsv_ffi/CLAUDE.md b/crates/tsv_ffi/CLAUDE.md index 3ef4c976f..d885fac91 100644 --- a/crates/tsv_ffi/CLAUDE.md +++ b/crates/tsv_ffi/CLAUDE.md @@ -4,7 +4,7 @@ ## Architecture Position -Depends on `tsv_ts`, `tsv_css`, `tsv_svelte`. Sibling binding crate: [`tsv_wasm`](../tsv_wasm/) (WebAssembly). Consumers include Deno FFI, Python `ctypes`, and any other C-FFI host. N-API is not used. +Depends on `tsv_ts`, `tsv_css`, `tsv_svelte`. Sibling binding crates: [`tsv_wasm`](../tsv_wasm/) (WebAssembly) and [`tsv_napi`](../tsv_napi/) (N-API, the Node/Bun native path). This crate is the C-ABI path — consumers include Deno FFI, Python `ctypes`, and any other C-FFI host. Node/Bun use `tsv_napi` instead (no C-FFI glue); the per-thread `with_ast_arena` reuse below is mirrored there. The bindings hold a **per-thread reusable AST `Bump`** (`with_ast_arena`) that is `reset()` between calls rather than allocated fresh per call: the bindings are invoked once per file in tight loops, and per-call arena malloc/free churns the system allocator's heap high-water in a way that is measurable through a host FFI layer. `reset()` retains the largest chunk and rewinds, so a warm thread does no per-call malloc/free; this replaces the old per-call `with_capacity` pre-size (hence no `tsv_lang` dependency). The per-file AST is fully consumed before the next call's `reset()`, so the reuse is sound (incl. after a `catch_unwind`-caught panic). diff --git a/crates/tsv_napi/CLAUDE.md b/crates/tsv_napi/CLAUDE.md new file mode 100644 index 000000000..cfc42e1fb --- /dev/null +++ b/crates/tsv_napi/CLAUDE.md @@ -0,0 +1,49 @@ +# tsv_napi + +> N-API bindings for `tsv`. Builds to a `cdylib` (`libtsv_napi.{so,dylib,dll}`) loaded by Node.js / Bun as a native addon, via [napi-rs](https://napi.rs). + +## Architecture Position + +Depends on `tsv_ts`, `tsv_css`, `tsv_svelte`. The **Node/Bun** sibling of the binding trio: [`tsv_ffi`](../tsv_ffi/) is Deno's C-FFI path (`Deno.dlopen`), [`tsv_wasm`](../tsv_wasm/) is the universal WASM path, and `tsv_napi` is the native path for the N-API runtimes. Same engine, same `lang_bindings!` shape, different binding boundary. + +This is a **tsv-scoped carve-out** from the ecosystem N-API deferral (decided 2026-06-13; the `napi` crate approved 2026-06-26). It is **not** an ecosystem-wide flip. + +Like `tsv_ffi`, the bindings hold a **per-thread reusable AST `Bump`** (`with_ast_arena`) that is `reset()` between calls rather than allocated fresh per call — the bindings are invoked once per file in tight loops, and per-call arena malloc/free churns the system allocator's heap high-water in a way that is measurable through a binding layer. The helper is currently **duplicated** from `tsv_ffi::with_ast_arena` (kept in lockstep by hand); factoring both onto one shared helper is a planned follow-up. + +Build/usage commands live in [../../CLAUDE.md §JS Bindings](../../CLAUDE.md#js-bindings). + +## Two-stage rollout + +- **(3a) measurement binding — done.** A single-platform local build (`deno task build:napi` → `cargo build -p tsv_napi --release`) drives the **Node** benchmark runner (`benches/js/lib/napi.ts` loads the built cdylib directly via `process.dlopen` — no `.node` rename). **No CI, no cross-platform matrix, no npm publish.** +- **(3b) publish matrix — not yet.** The cross-platform prebuilt `.node` artifacts (per-platform `optionalDependencies` under a thin `@fuzdev/tsv_napi` loader) + release CI are deferred, targeted for the 0.2 release. Do **not** bolt N-API onto the single-machine `deno task publish`. + +## Features + +Mirrors `tsv_ffi` / `tsv_wasm`: + +- `format` (default) — `format_` exports +- `parse` (default) — `parse_` + `parse_internal_` exports, and the `convert` layer on each language crate + +## Public API + +The `lang_bindings!` macro generates three `#[napi]` functions per language (svelte, typescript, css); the `format`/`parse` features gate which are emitted: + +- `parse_(source) -> string` — JSON AST string (host `JSON.parse`s it — parity with FFI/WASM) +- `parse_internal_(source) -> void` — parses without converting (benchmark-only; `black_box` prevents elision) +- `format_(source) -> string` — formatted source + +JS export names are kept **snake_case** via `#[napi(js_name = "…")]` (napi-rs would otherwise camelCase them) so the addon's surface matches `tsv_wasm`'s. + +## Marshalling & errors + +napi-rs marshals the JS string into a Rust `String` and the returned `String` back out — **no raw pointers, no manual free** (unlike `tsv_ffi`). Engine errors are returned as `napi::Result::Err(napi::Error)`, which napi-rs converts to a **thrown JS error** — there is no `{"error": …}` envelope to inspect (the FFI shape); a throw just propagates. + +**Panic profile:** the published `[profile.release]` is `panic = "abort"`, so a Rust panic aborts the process (napi-rs can only catch panics under `panic = "unwind"`). The measurement binding ships release for native-vs-native parity with `tsv_ffi`; the bench corpus is curated so tsv doesn't panic. If a panic-tolerant build is needed, the `[profile.corpus]` (`panic = "unwind"`) precedent exists. + +## Files + +- `src/lib.rs` — All bindings: `with_ast_arena`, the `lang_bindings!` macro, the three `lang_bindings!` invocations, and a `#[cfg(test)]` module +- `build.rs` — `napi_build::setup()` (linker config for the addon) +- `Cargo.toml` — `crate-type = ["cdylib"]`; `unsafe_code = "allow"` (N-API generates unsafe code); deps `napi` + `napi-derive` (3.x), build-dep `napi-build` (2.x) + +The in-crate test module drives every entry point in-process (`cargo test` exercises the native binding without a Node host — the Deno/WASM smoke paths don't cover napi). Node-side marshalling is covered by the bench's Node runner (and, at 3b, a `scripts/test_napi.ts`). diff --git a/crates/tsv_napi/Cargo.toml b/crates/tsv_napi/Cargo.toml new file mode 100644 index 000000000..d52cf873c --- /dev/null +++ b/crates/tsv_napi/Cargo.toml @@ -0,0 +1,109 @@ +[package] +name = "tsv_napi" +version.workspace = true +edition.workspace = true +description = "TypeScript and Svelte tools - N-API bindings (Node.js/Bun)" +license.workspace = true +repository.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +tsv_svelte = { workspace = true } +tsv_ts = { workspace = true } +tsv_css = { workspace = true } +napi = { workspace = true } +napi-derive = { workspace = true } +serde_json.workspace = true +bumpalo.workspace = true + +[build-dependencies] +napi-build = { workspace = true } + +[features] +# Mirrors tsv_ffi / tsv_wasm: `format` exports the formatters, `parse` exports +# the parsers plus the public-AST/JSON convert layer they serialize through. The +# default both-features build is what the Node bench loads. +default = ["format", "parse"] +format = [] +parse = ["tsv_ts/convert", "tsv_css/convert", "tsv_svelte/convert"] + +# N-API generates unsafe code, exactly like the FFI crate. Cargo replaces the +# whole [lints] table on override (no partial inherit), so the workspace lints +# are re-declared verbatim below — only `unsafe_code` differs. Keep in sync with +# the root [workspace.lints] block; the restriction lints (panic/unwrap_used/etc.) +# must stay so this crate keeps the same floor as the rest of the workspace. +[lints.rust] +unsafe_code = "allow" +missing_debug_implementations = "allow" +trivial_casts = "warn" +trivial_numeric_casts = "warn" +unused_lifetimes = "warn" +unused_qualifications = "warn" + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +# Pedantic overrides +module_name_repetitions = "allow" +must_use_candidate = "allow" +similar_names = "allow" +too_many_lines = "allow" +use_self = "allow" +doc_markdown = "allow" +unreadable_literal = "allow" +option_if_let_else = "allow" +if_not_else = "allow" +match_same_arms = "allow" +single_match_else = "allow" +wildcard_imports = "allow" +unused_self = "allow" +unnested_or_patterns = "allow" +explicit_iter_loop = "allow" +match_wildcard_for_single_variants = "allow" +needless_pass_by_value = "allow" +bool_to_int_with_if = "allow" +unnecessary_wraps = "allow" +fn_params_excessive_bools = "allow" +comparison_chain = "allow" +large_enum_variant = "allow" +too_long_first_doc_paragraph = "allow" +struct_field_names = "allow" +items_after_statements = "allow" +implicit_hasher = "allow" +doc_link_with_quotes = "allow" +return_self_not_must_use = "allow" +case_sensitive_file_extension_comparisons = "allow" +needless_continue = "allow" +unnecessary_debug_formatting = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +cast_possible_truncation = "allow" +cast_lossless = "allow" +cast_possible_wrap = "allow" +# Nursery overrides +significant_drop_tightening = "allow" +missing_const_for_fn = "allow" +redundant_pub_crate = "allow" +branches_sharing_code = "allow" +cognitive_complexity = "allow" +future_not_send = "allow" +unnecessary_literal_bound = "allow" +equatable_if_let = "allow" +useless_let_if_seq = "allow" +elidable_lifetime_names = "allow" +# Cargo overrides +cargo_common_metadata = "allow" # internal crates, not published to crates.io +# hashbrown 0.15 (string-interner) vs 0.16 (serde_json preserve_order -> indexmap); +# unresolvable until string-interner bumps upstream +multiple_crate_versions = "allow" +# Restriction lints (panic points need explicit #[allow] with justification) +clone_on_ref_ptr = "warn" +dbg_macro = "warn" +expect_used = "warn" +panic = "warn" +todo = "warn" +unwrap_used = "warn" diff --git a/crates/tsv_napi/build.rs b/crates/tsv_napi/build.rs new file mode 100644 index 000000000..eb9d2e912 --- /dev/null +++ b/crates/tsv_napi/build.rs @@ -0,0 +1,7 @@ +//! N-API build setup. Configures the linker for the `.node` cdylib (e.g. +//! `-undefined dynamic_lookup` on macOS so the addon resolves Node symbols at +//! load time). A near no-op on Linux, but the idiomatic napi-rs entry point. + +fn main() { + napi_build::setup(); +} diff --git a/crates/tsv_napi/src/lib.rs b/crates/tsv_napi/src/lib.rs new file mode 100644 index 000000000..f614f92c0 --- /dev/null +++ b/crates/tsv_napi/src/lib.rs @@ -0,0 +1,187 @@ +//! N-API bindings for tsv (Node.js / Bun). +//! +//! The Node-runtime sibling of [`tsv_ffi`](../tsv_ffi) (the Deno/C-FFI path) and +//! [`tsv_wasm`](../tsv_wasm) (the universal WASM path). napi-rs marshals the +//! JS string into a Rust `String` and the returned `String` back out, so there +//! are no raw pointers and no manual free — the cleanest of the three bindings. +//! +//! Transport mirrors `tsv_wasm`'s deliberate choice: `parse_` returns a +//! JSON **string** for the host to `JSON.parse`, rather than building the object +//! graph node-by-node across the boundary (measurably slower). `format_` +//! returns the formatted source directly. Engine errors surface as thrown JS +//! errors (`napi::Error`); `parse_internal_` parses without converting +//! (benchmark-only, AST kept live via `black_box`). +//! +//! Built as a `cdylib` and loaded by Node as a `.node` addon. The `format` / +//! `parse` cargo features gate which entry points are emitted (mirrors +//! `tsv_ffi` / `tsv_wasm`). + +use napi_derive::napi; + +/// Run `f` with a per-thread reusable AST arena. +/// +/// Duplicates `tsv_ffi`'s `with_ast_arena` (see its rationale): the bindings are +/// called once per file in tight loops, so a fresh `Bump` per call churns the +/// system allocator's heap high-water in a way that is measurable through the +/// binding boundary. Each thread keeps one `Bump` and `reset()`s it between +/// calls — `reset()` rewinds the bump pointer and retains the largest chunk, so +/// a warm thread does no per-call malloc/free. The per-file AST is fully consumed +/// inside `f` (the returned value owns its bytes — a formatted `String`, a JSON +/// `String`, or `()`), so no AST outlives the next call's `reset()`. +/// +/// Kept in lockstep with `tsv_ffi::with_ast_arena` by hand for now; factoring +/// both onto one shared helper is a follow-up (lore TODO_NAPI_BINDINGS §arena +/// reuse / TODO_BUMPALO_ARENA item 1). +fn with_ast_arena(f: impl FnOnce(&bumpalo::Bump) -> R) -> R { + thread_local! { + static AST_ARENA: std::cell::RefCell = + std::cell::RefCell::new(bumpalo::Bump::new()); + } + AST_ARENA.with(|cell| { + let mut arena = cell.borrow_mut(); + arena.reset(); + f(&arena) + }) +} + +/// Generate `parse_` / `parse_internal_` / `format_` N-API +/// functions for one language module. The `js_name` literals keep the JS export +/// names snake_case for parity with `tsv_wasm` (napi-rs would otherwise +/// camelCase them). +macro_rules! lang_bindings { + ( + $lang:ident, + $parse_fn:ident, $parse_js:literal, + $parse_internal_fn:ident, $parse_internal_js:literal, + $format_fn:ident, $format_js:literal + ) => { + /// Parse source code and return its public JSON AST as a string. + #[cfg(feature = "parse")] + #[napi(js_name = $parse_js)] + pub fn $parse_fn(source: String) -> napi::Result { + with_ast_arena(|arena| { + let ast = $lang::parse(&source, arena) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok($lang::convert_ast_json_string(&ast, &source)) + }) + } + + /// Parse source to the internal AST only (no conversion, no + /// serialization). Benchmark-only: `black_box` keeps the AST live so the + /// parse can't be optimized away. + #[cfg(feature = "parse")] + #[napi(js_name = $parse_internal_js)] + pub fn $parse_internal_fn(source: String) -> napi::Result<()> { + with_ast_arena(|arena| { + let ast = $lang::parse(&source, arena) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + std::hint::black_box(&ast); + Ok(()) + }) + } + + /// Format source code and return the formatted string. + #[cfg(feature = "format")] + #[napi(js_name = $format_js)] + pub fn $format_fn(source: String) -> napi::Result { + with_ast_arena(|arena| { + let ast = $lang::parse(&source, arena) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok($lang::format(&ast, &source)) + }) + } + }; +} + +lang_bindings!( + tsv_svelte, + parse_svelte, + "parse_svelte", + parse_internal_svelte, + "parse_internal_svelte", + format_svelte, + "format_svelte" +); +lang_bindings!( + tsv_ts, + parse_typescript, + "parse_typescript", + parse_internal_typescript, + "parse_internal_typescript", + format_typescript, + "format_typescript" +); +lang_bindings!( + tsv_css, + parse_css, + "parse_css", + parse_internal_css, + "parse_internal_css", + format_css, + "format_css" +); + +// Drive every entry point in-process so `cargo test` exercises the native +// binding without a Node host (the Deno/WASM smoke paths don't cover napi). +// These call the plain Rust functions the `#[napi]` macro wraps; the JS +// marshalling layer is what `scripts/test_napi.ts` covers under Node. +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used)] +mod tests { + use super::*; + + fn error_message(err: &napi::Error) -> String { + err.reason.clone() + } + + // --- format: happy path (one per language exercises the macro expansion) --- + + #[test] + fn format_typescript_normalizes() { + assert_eq!( + format_typescript("const x=1".to_owned()).unwrap(), + "const x = 1;\n" + ); + } + + #[test] + fn format_css_normalizes() { + assert_eq!( + format_css("a{color:red}".to_owned()).unwrap(), + "a {\n\tcolor: red;\n}\n" + ); + } + + #[test] + fn format_svelte_normalizes() { + assert_eq!( + format_svelte("
x
".to_owned()).unwrap(), + "
x
\n" + ); + } + + // --- parse: returns JSON AST; internal returns unit --- + + #[test] + fn parse_typescript_returns_json() { + let json = parse_typescript("const x = 1;".to_owned()).unwrap(); + let value: serde_json::Value = serde_json::from_str(&json).unwrap(); + assert_eq!( + value.get("type").and_then(serde_json::Value::as_str), + Some("Program") + ); + } + + #[test] + fn parse_internal_css_ok() { + parse_internal_css("a { color: red }".to_owned()).unwrap(); + } + + // --- errors surface as napi::Error with the engine message --- + + #[test] + fn format_typescript_invalid_errors() { + let err = format_typescript("const = ;".to_owned()).unwrap_err(); + assert!(!error_message(&err).is_empty(), "error must carry a reason"); + } +} diff --git a/deno.json b/deno.json index 063e9aa9f..1e82e6a7c 100644 --- a/deno.json +++ b/deno.json @@ -8,10 +8,14 @@ "build:ffi:format": "cargo build -p tsv_ffi --release --no-default-features --features format --target-dir target/ffi-format", "build:ffi:parse": "cargo build -p tsv_ffi --release --no-default-features --features parse --target-dir target/ffi-parse", "build:ffi:all": "deno task build:ffi && deno task build:ffi:format && deno task build:ffi:parse", + "build:napi": "cargo build -p tsv_napi --release", "build:wasm:deno": "wasm-pack build crates/tsv_wasm --target deno --release --out-dir pkg/format/deno -- --no-default-features --features format", "build:wasm:parse:deno": "wasm-pack build crates/tsv_wasm --target deno --release --out-dir pkg/parse/deno -- --no-default-features --features parse", "build:wasm:all:deno": "wasm-pack build crates/tsv_wasm --target deno --release --out-dir pkg/all/deno", - "build:bench": "deno task build:ffi:all && deno task build:wasm:deno && deno task build:wasm:parse:deno && deno task build:wasm:all:deno", + "build:wasm:all:nodejs": "wasm-pack build crates/tsv_wasm --target nodejs --release --out-dir pkg/all/nodejs", + "build:bench": "deno task build:bench:deno && deno task build:bench:node", + "build:bench:deno": "deno task build:ffi:all && deno task build:wasm:deno && deno task build:wasm:parse:deno && deno task build:wasm:all:deno", + "build:bench:node": "deno task build:napi && deno task build:wasm:all:nodejs", "build:npm:format": "wasm-pack build crates/tsv_wasm --target web --release --out-dir pkg/format/npm -- --no-default-features --features format && deno run --allow-read --allow-write=crates/tsv_wasm/pkg --allow-run=gzip scripts/patch_npm_package.ts format", "build:npm:parse": "wasm-pack build crates/tsv_wasm --target web --release --out-dir pkg/parse/npm -- --no-default-features --features parse && deno run --allow-read --allow-write=crates/tsv_wasm/pkg --allow-run=gzip scripts/patch_npm_package.ts parse", "build:npm:all": "wasm-pack build crates/tsv_wasm --target web --release --out-dir pkg/all/npm && deno run --allow-read --allow-write=crates/tsv_wasm/pkg --allow-run=gzip scripts/patch_npm_package.ts all", @@ -37,17 +41,26 @@ "scan:audit": "cargo run -p tsv_debug --quiet scan_audit", "metrics": "cargo run -p tsv_debug --quiet metrics", - "bench": "deno task build:bench && deno task bench:run", - "bench:run": "deno run --v8-flags=--expose-gc --allow-ffi --allow-read --allow-write=benches/deno/results --allow-run=git,gzip --allow-env --allow-net --allow-sys benches/deno/bench.ts", - "bench:clean": "rm -f benches/deno/results/baseline.json benches/deno/results/[0-9]*.json benches/deno/results/[0-9]*.md", - "smoke": "deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys benches/deno/smoke.ts", + "bench:install": "node benches/js/install_deps.ts", + "bench": "deno task bench:deno && deno task bench:node && deno task bench:bun:run && deno task bench:compose", + "bench:deno": "deno task build:bench:deno && deno task bench:deno:run", + "bench:deno:run": "deno run --v8-flags=--expose-gc --allow-ffi --allow-read --allow-write=benches/js/results --allow-run=git,gzip --allow-env --allow-net --allow-sys benches/js/bench.ts", + "bench:node": "deno task build:bench:node && deno task bench:node:run", + "bench:node:run": "node --expose-gc benches/js/bench.ts", + "bench:bun": "deno task build:bench:node && deno task bench:bun:run", + "bench:bun:run": "bun --expose-gc benches/js/bench.ts", + "bench:compose": "deno run --allow-read --allow-write=benches/js/results benches/js/compose_reports.ts", + "bench:clean": "rm -f benches/js/results/baseline.json benches/js/results/[0-9]*.json benches/js/results/[0-9]*.md", + "smoke": "deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys benches/js/smoke.ts", + "smoke:node": "node benches/js/smoke.ts", + "smoke:bun": "bun benches/js/smoke.ts", "build:ffi:corpus": "cargo build -p tsv_ffi --profile corpus", "corpus:compare:format": "deno task build:ffi:corpus && TSV_FFI_PROFILE=corpus deno task corpus:compare:format:run", - "corpus:compare:format:run": "PRETTIER_DEBUG=1 deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys benches/deno/corpus_compare_format.ts", + "corpus:compare:format:run": "PRETTIER_DEBUG=1 deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys benches/js/corpus_compare_format.ts", "corpus:compare:parse": "deno task build:ffi:corpus && TSV_FFI_PROFILE=corpus deno task corpus:compare:parse:run", - "corpus:compare:parse:run": "deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys benches/deno/corpus_compare_parse.ts", - "divergence:audit": "deno run --allow-read benches/deno/divergence_audit.ts", - "test:deno": "deno test --config benches/deno/deno.json --allow-read --allow-env --allow-net --allow-sys benches/deno/lib/", + "corpus:compare:parse:run": "deno run --allow-ffi --allow-read --allow-env --allow-net --allow-sys benches/js/corpus_compare_parse.ts", + "divergence:audit": "deno run --allow-read benches/js/divergence_audit.ts", + "test:deno": "deno test --config benches/js/deno.json --allow-read --allow-env --allow-net --allow-sys benches/js/lib/", "test:npm": "deno task build:npm:format && deno task test:npm:run", "test:npm:run": "PKG_DIR=crates/tsv_wasm/pkg/format/npm node --test scripts/test_npm.ts", diff --git a/docs/cli.md b/docs/cli.md index 519a51a9d..f5fdc6f0f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -49,7 +49,7 @@ The CLI uses [argh](https://crates.io/crates/argh) for declarative arg parsing: - Used by: `canonical_parse`, `ast_diff`, fixture management - Purpose: Parse TypeScript code (matches Svelte's TS parser) -Versions are pinned (exact) in `crates/tsv_debug/src/deno/sidecar.ts` — the source of truth; they are not repeated here. `benches/deno/deno.json` pins the same versions independently for the bench harness; keep the two in sync. +Versions are pinned (exact) in `crates/tsv_debug/src/deno/sidecar.ts` — the source of truth; they are not repeated here. `benches/js/package.json` pins the same versions independently for the bench harness; keep the two in sync. ## Input Handling diff --git a/docs/conformance_prettier.md b/docs/conformance_prettier.md index 416f2be11..1b7510bda 100644 --- a/docs/conformance_prettier.md +++ b/docs/conformance_prettier.md @@ -822,7 +822,7 @@ deno task corpus:compare:format ~/dev/project --explain # Single project (scans deno task divergence:audit # Cross-refs pattern fixture lists vs this doc (no runtime) ``` -Every pattern in `benches/deno/lib/divergence/patterns.ts` links to: +Every pattern in `benches/js/lib/divergence/patterns.ts` links to: - `conformance_sections` — Section names from this document - `fixtures` — Fixture paths the pattern detects (enforced by the behavioral diff --git a/docs/conformance_svelte.md b/docs/conformance_svelte.md index bf67ee4dc..e45b46ff6 100644 --- a/docs/conformance_svelte.md +++ b/docs/conformance_svelte.md @@ -39,7 +39,7 @@ Cases where tsv intentionally produces different AST than Svelte. Fixtures use ` **Corpus-scale enforcement**: `deno task corpus:compare:parse` deep-diffs tsv's parse output against the canonical parsers on real codebases and classifies diffs against this catalog (the `DOCUMENTED_MATCHERS` list in -`benches/deno/corpus_compare_parse.ts` covers the divergences that parse on +`benches/js/corpus_compare_parse.ts` covers the divergences that parse on both sides). Keep the two in sync: a new documented AST divergence gets a matcher, and an unmatched corpus diff group is either a bug or a missing catalog entry. @@ -125,7 +125,7 @@ construct abort an otherwise-valid _standard-CSS_ file; these dialects are input tsv never chases regardless. Non-standard `.css` is auto-classified into `expected errors` by the corpus -comparator (`benches/deno/lib/divergence/expected_errors.ts`). +comparator (`benches/js/lib/divergence/expected_errors.ts`). ### Svelte Template Corrections (corpus-enforced) @@ -197,7 +197,7 @@ and formatting is unaffected. Fixture: Intentional AST divergences from acorn-typescript that have no prettier-stable fixture form (prettier rewrites the triggering syntax), so the corpus parse differential enforces them via `DOCUMENTED_MATCHERS` in -`benches/deno/corpus_compare_parse.ts` instead. +`benches/js/corpus_compare_parse.ts` instead. **Rest param type-annotation end** (`rest_param_type_end`): acorn-typescript ends a typed `RestElement` at the binding (`(...args: Array)` → `end` diff --git a/docs/conformance_test262.md b/docs/conformance_test262.md index c8c96a1a9..a4f3f645c 100644 --- a/docs/conformance_test262.md +++ b/docs/conformance_test262.md @@ -210,8 +210,8 @@ cargo run -p tsv_debug test262 --emit-manifest /tmp/t262.json # 2. Deno consumer runs oxc-parser over the same files (parsed as a module, to # mirror tsv) and buckets the agreement. Run from the repo root: deno run --allow-read --allow-env --allow-ffi --allow-net --allow-sys \ - --config benches/deno/deno.json \ - benches/deno/diagnostics/test262_compare.ts --manifest /tmp/t262.json + --config benches/js/deno.json \ + benches/js/diagnostics/test262_compare.ts --manifest /tmp/t262.json ``` **Why oxc-parser only (no biome).** test262 is a *parse*-conformance suite, and diff --git a/docs/divergence_detector.md b/docs/divergence_detector.md index a25fc6565..1c115b477 100644 --- a/docs/divergence_detector.md +++ b/docs/divergence_detector.md @@ -7,7 +7,7 @@ The divergence detector is integrated into `corpus_compare_format.ts` and automa ## Architecture ``` -benches/deno/ +benches/js/ ├── corpus_compare_format.ts # Uses divergence detection ├── divergence_audit.ts # Cross-references patterns vs docs ├── lib/ @@ -192,7 +192,7 @@ deno task test:deno # unit + behavioral tests 1. Document the divergence in `docs/conformance_prettier.md` 2. Create fixture in `tests/fixtures/.../..._prettier_divergence/` -3. Add pattern to `benches/deno/lib/divergence/patterns.ts`: +3. Add pattern to `benches/js/lib/divergence/patterns.ts`: ```typescript const new_pattern: DivergencePattern = { @@ -224,7 +224,7 @@ const new_pattern: DivergencePattern = { ``` 4. Add positive + negative (overmatch-rejection) tests to - `benches/deno/lib/divergence/patterns_test.ts`. Only list a fixture in `fixtures` if the + `benches/js/lib/divergence/patterns_test.ts`. Only list a fixture in `fixtures` if the pattern genuinely detects it — the behavioral fixture-coverage audit fails otherwise, so don't list a divergence the pattern structurally can't claim (reassign it to the pattern that does, or leave it honestly uncovered). diff --git a/docs/fixture_overview.md b/docs/fixture_overview.md index 714ea2cab..c7e9ec8c2 100644 --- a/docs/fixture_overview.md +++ b/docs/fixture_overview.md @@ -373,7 +373,7 @@ All fixtures use `input.svelte` as canonical source. P1–P3 anchor fixtures to the canonical parsers; P4/P5 prove tsv's two materialization paths agree with each other. A bug shared by BOTH paths is invisible to all of them — the corpus-scale external oracle for that class is -`deno task corpus:compare:parse` (../benches/deno/CLAUDE.md §Parse +`deno task corpus:compare:parse` (../benches/js/CLAUDE.md §Parse Comparison), which deep-diffs the shipped wire against the canonical parsers on real codebases. diff --git a/docs/performance.md b/docs/performance.md index 74131aeac..ad4ed2bef 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -257,7 +257,7 @@ target-independent (heaptrack reads the same on either), but WASM *wall-time* is not: `@fuzdev/tsv_format_wasm` runs on dlmalloc, which memcpys on every heap growth, so an allocation-count win can move WASM format time even when the same change is a wash on native glibc. The full `deno task bench` is too coarse to -see those single-digit-% moves; `benches/deno/diagnostics/wasm_format_probe.ts` resolves +see those single-digit-% moves; `benches/js/diagnostics/wasm_format_probe.ts` resolves them. It applies the §5 paired discipline in a single invocation: interleaved pairs @@ -274,7 +274,7 @@ cp -r crates/tsv_wasm/pkg/all/deno crates/tsv_wasm/pkg/all/deno.baseline # ... edit source, then rebuild and A/B: deno task build:wasm:all:deno deno run --allow-read --allow-env --allow-net --allow-sys \ - benches/deno/diagnostics/wasm_format_probe.ts \ + benches/js/diagnostics/wasm_format_probe.ts \ --baseline crates/tsv_wasm/pkg/all/deno.baseline/tsv_wasm.js ``` @@ -289,7 +289,7 @@ before starting an A/B. ```bash deno run --allow-read --allow-env --allow-net --allow-sys \ - benches/deno/diagnostics/wasm_format_probe.ts + benches/js/diagnostics/wasm_format_probe.ts ``` ## Measurement Process @@ -304,7 +304,7 @@ deno run --allow-read --allow-env --allow-net --allow-sys \ 1. **`tsv_debug profile`** — same workload, compare phase split 2. **`deno task bench`** — measure overall corpus impact -3. **Record results** — for regression detection, use `deno task bench:run --save-baseline` / `--compare-baseline` +3. **Record results** — for regression detection, use `deno task bench:deno:run -- --save-baseline` / `-- --compare-baseline` (or `bench:node:run` for the Node runtime) ## WASM bundle size @@ -318,7 +318,7 @@ The `tsv_wasm` crate produces three WASM binaries via the `format` + `binary_sizes.ts` in the bench runner reads the three `pkg//deno/tsv_wasm_bg.wasm` files and reports them side-by-side, with gzipped wire size alongside raw on-disk size; current numbers land in the bench -report (`benches/deno/results/report.md`). +report (`benches/js/results/report..md`). Gzipped numbers come from `gzip -c` (system default level 6), matching npm-tarball wire reality and `scripts/patch_npm_package.ts`. The parse feature @@ -346,7 +346,8 @@ These aren't set up yet but may be useful for specific investigations: ## Baselines and tracking Methodology and tooling above are evergreen; corpus benchmark results land in -`benches/deno/results/report.{json,md}`. +the per-runtime `benches/js/results/report..{json,md}` siblings +(`report.deno.*` / `report.node.*`). Wall-clock readings vary several-fold with machine state (CPU frequency scaling and concurrent load) — trust only quiet-machine runs, and prefer per-byte rates diff --git a/docs/workflow_corpus.md b/docs/workflow_corpus.md index 6d2e103b9..1cafedf60 100644 --- a/docs/workflow_corpus.md +++ b/docs/workflow_corpus.md @@ -5,7 +5,7 @@ This doc covers the **formatting** comparison (`corpus:compare:format`, vs prettier). The parser-side analogue — `corpus:compare:parse`, deep-diffing parse ASTs against the canonical parsers — is documented in -../benches/deno/CLAUDE.md §Parse Comparison; its diffs are triaged with the +../benches/js/CLAUDE.md §Parse Comparison; its diffs are triaged with the fixture-first TDD flow rather than this file's hunk workflow. ## First Step: Load Conformance Doc @@ -59,7 +59,7 @@ The divergence detector already identifies this pattern. Nothing to do. **B) Known divergence but detector misses it** → Fix the detector The diff matches a documented pattern in `conformance_prettier.md`, but the detector in -`benches/deno/lib/divergence/patterns.ts` doesn't recognize this variant. +`benches/js/lib/divergence/patterns.ts` doesn't recognize this variant. 1. Identify which existing pattern should match (e.g., `inline_content_hug`, `fill_101_boundary`) 2. Read the pattern's `detect()` function in `patterns.ts` From 9c4b66f57cd518704b0d51b7fc11d2758be815ca Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 26 Jun 2026 14:56:58 -0400 Subject: [PATCH 2/2] simplify benchmarks and improve docs and cleanup dev stuff --- .github/ISSUE_TEMPLATE/preapproved.md | 6 +- .github/workflows/check.yml | 10 +- CLAUDE.md | 19 +- benches/js/CLAUDE.md | 88 +-- benches/js/bench.ts | 3 + benches/js/deno.json | 12 +- benches/js/deno.lock | 537 ------------------ benches/js/diagnostics/acorn_dup_fuzz.ts | 11 +- benches/js/diagnostics/comment_dup_scan.ts | 16 +- benches/js/diagnostics/test262_compare.ts | 2 +- benches/js/lib/biome.ts | 7 +- benches/js/lib/canonical_test.ts | 2 +- .../js/lib/divergence/expected_errors_test.ts | 2 +- .../lib/divergence/fixture_coverage_test.ts | 2 +- benches/js/lib/divergence/patterns_test.ts | 2 +- benches/js/lib/divergence/safety_test.ts | 2 +- benches/js/package.json | 2 +- crates/tsv_napi/CLAUDE.md | 2 +- deno.json | 3 +- deno.lock | 11 - 20 files changed, 110 insertions(+), 629 deletions(-) delete mode 100644 benches/js/deno.lock delete mode 100644 deno.lock diff --git a/.github/ISSUE_TEMPLATE/preapproved.md b/.github/ISSUE_TEMPLATE/preapproved.md index 9e02e4527..f0e3dd694 100644 --- a/.github/ISSUE_TEMPLATE/preapproved.md +++ b/.github/ISSUE_TEMPLATE/preapproved.md @@ -1,8 +1,8 @@ --- name: New issue (bugs only) -title: (bug reports only, make a discussion for everything else) +title: (bugs only, see below) about: bugs --- -tip: use discussions for everything except bugs: -https://github.com/fuzdev/tsv/discussions/new/choose +Please [create a discussion](https://github.com/fuzdev/tsv/discussions/new/choose) +for everything except bugs. diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6ddb4a2fc..46aa15376 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -69,10 +69,12 @@ jobs: # npm fetches (prettier/svelte/acorn) since those land in DENO_DIR. cache: true # Key on the sidecar pins (fetched at runtime, not lockfile-pinned) so a - # bump invalidates the cached deps. NOT benches/js/deno.lock: it governs - # `test:deno` but is dominated by bench-only tools (biome/oxc/oxfmt) that - # `check` never fetches, so keying on it would bust the cache on unrelated - # bumps. (There is no root deno.lock — the root config has no external deps.) + # bump invalidates the cached deps. The sidecar's prettier/svelte fetches + # are the only runtime npm `check` does: `test:deno` runs the + # dependency-free divergence suite (node: builtins + relative imports, no + # node_modules — proven by a clean checkout), and the bench's heavy npm + # tree (biome/oxc/oxfmt) is never installed here. No deno.lock files exist + # (the bench resolves npm from package-lock.json; root has no external deps). cache-hash: ${{ hashFiles('crates/tsv_debug/src/deno/sidecar.ts', 'crates/tsv_debug/src/deno/actor.rs') }} - name: Check diff --git a/CLAUDE.md b/CLAUDE.md index d45116373..9be3f6aec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -288,11 +288,13 @@ deno task bench:install # Smoke test (Deno; fast sanity check that every formatter+parser produces output) deno task smoke -# Run benchmarks (builds the runtime's bench artifacts automatically) -deno task bench # ALL three + compose → report.{deno,node,bun}.* + combined report.{json,md} -deno task bench:deno # Deno only -deno task bench:node # Node only -deno task bench:bun # Bun only (reuses the Node artifacts) +# Run benchmarks (builds the runtime's bench artifacts automatically). +# `bench` runs ALL three and FAILS FAST if node or bun is missing — Deno is the +# only hard dep, so without node/bun run the per-runtime tasks you have instead. +deno task bench # ALL three + compose (needs node AND bun installed) +deno task bench:deno # Deno only (no node/bun needed) +deno task bench:node # Node only (needs node) +deno task bench:bun # Bun only (needs bun; reuses the Node artifacts) deno task bench:compose # Fold existing per-runtime reports → combined report.{json,md} # Run without rebuilding (if already built; aborts on stale artifacts) @@ -313,7 +315,12 @@ BENCH_STALE_OK=1 deno task bench:deno:run # Run despite stale artifacts (de BENCH_FORCED_ASYNC=1 deno task bench:deno:run # Add tsv-forced-async control row (diagnostic; default: off) ``` -**Prerequisites**: `cargo install wasm-pack`; Node ≥ 22.18 (native TS type-stripping); Bun (for `bench:bun`); `deno task bench:install` once. +**Prerequisites**: `cargo install wasm-pack` and `deno task bench:install` once +(the install needs `npm`, i.e. Node, to populate `node_modules`). Beyond that, +**Deno is the only hard dependency** — `bench:deno` / `smoke` run with just Deno. +Node ≥ 22.18 (native TS type-stripping) is needed for `bench:node` (and the +install); Bun for `bench:bun`. The aggregate `deno task bench` needs both and +fails fast if either is missing; run the per-runtime tasks for the ones you have. Compares: canonical (prettier + svelte/compiler), native (FFI under Deno / N-API under Node and Bun), WASM, and alternatives (oxc-parser, oxfmt, biome-wasm). Each runtime's results save diff --git a/benches/js/CLAUDE.md b/benches/js/CLAUDE.md index 17bfcacbc..28f486fe9 100644 --- a/benches/js/CLAUDE.md +++ b/benches/js/CLAUDE.md @@ -4,11 +4,10 @@ Uses [@fuzdev/fuz_util](https://github.com/fuzdev/fuz_util) benchmarking library for statistical analysis. -> **Directory note:** this directory (renamed from the legacy `benches/deno/` — -> the harness is JS-runtime-agnostic, not Deno's) is the **runtime-neutral** -> harness home: the same code runs under Deno, Node, and Bun (see -> [Cross-Runtime](#cross-runtime-deno--node--bun)). The `corpus_compare_*` and -> `diagnostics/` entries stay Deno-idiomatic; `smoke` is portable across all +> **Directory note:** this is the **runtime-neutral** JS benchmark harness — +> named `js` (not `deno`) because the same code runs under Deno, Node, and Bun +> (see [Cross-Runtime](#cross-runtime-deno--node--bun)). The `corpus_compare_*` +> and `diagnostics/` entries stay Deno-idiomatic; `smoke` is portable across all > three (`smoke` / `smoke:node` / `smoke:bun`). ## Cross-Runtime (Deno + Node + Bun) @@ -36,9 +35,11 @@ sensitivity that mismeasured the native path (see §Known Issues + the lore (Deno supports them) + `@fuzdev/fuz_util` helpers (`fs_search`, `fs_exists`, `spawn_out`, `to_file_path`) — **no `Deno.*`, no `@std/*`**. The only genuinely runtime-specific files are the native loader (`ffi.ts` `Deno.dlopen` vs - `napi.ts` `process.dlopen`) and the WASM target the loader picks. Deno-only - tools (`corpus_compare_*`, `diagnostics/*`, the `deno test` suite) stay - Deno-idiomatic (`Deno.*` / `@std/fs`). + `napi.ts` `process.dlopen`) and the WASM target the loader picks. The + Deno-only entry points (`corpus_compare_*`, `diagnostics/*`) stay + Deno-idiomatic (`Deno.*` + `node:` builtins, no `@std/*`). The `deno test` + suite is the dependency-free divergence detectors (`node:assert` + relative + imports — see §Divergence Detection). - **The native row differs by runtime, fairly.** Deno → FFI (`tsv_ffi`, via `Deno.dlopen`); Node → N-API (`tsv_napi`, via `process.dlopen`). Same engine, same per-thread arena reuse, different binding boundary. @@ -49,10 +50,12 @@ sensitivity that mismeasured the native path (see §Known Issues + the lore **Dependencies: `package.json` is the source of truth.** Both runtimes consume one `node_modules` (`benches/js/package.json`). Deno reads it via -`"nodeModulesDir": "manual"` in `deno.json`; Node reads it directly. Only jsr-only -specifiers (`@std/fs`, used by the Deno-only diagnostics) stay in the deno.json -import map. `@types/node` is a types-only devDependency so the `node:` builtins -type-check under `deno check`/`deno test`. +`"nodeModulesDir": "manual"` in `deno.json`; Node reads it directly. There are no +jsr or remote deps — the bench and the diagnostics import npm packages by bare +specifier (resolved from `package.json`) and otherwise use `node:` builtins, so +`deno.json` carries only `nodeModulesDir: manual` + `lock: false` (no `deno.lock`; +npm integrity is `package-lock.json`'s job). `@types/node` is a types-only +devDependency so the `node:` builtins type-check under `deno check`. **Install with `deno task bench:install`** (runs `benches/js/install_deps.ts`). It is the canonical installer: a plain `npm install` works but **prunes the @@ -251,15 +254,21 @@ Patterns are ordered specific to broad. Each links to `conformance_sections` and deno task divergence:audit # Human-readable report deno task divergence:audit --json # Machine-readable JSON -# Deno test suite — every *_test.ts under lib/, gated by `deno task check`. Covers -# the divergence detectors (pattern positive/negative overmatch-rejection cases, +# Deno test suite — the divergence detectors under lib/divergence/, gated by +# `deno task check`. Covers pattern positive/negative overmatch-rejection cases, # safety differential cases, and a behavioral fixture-coverage audit that drives # each detector against its own committed fixtures — input == ours, output_prettier -# == prettier — failing if a pattern stops claiming a hunk in a fixture it lists) -# plus canonical_test.ts (asserts the prettier baseline formats with a filepath, so -# `.ts` single-type-param arrows stay `` and `.svelte` ones get ``). +# == prettier — failing if a pattern stops claiming a hunk in a fixture it lists. +# These are dependency-free (`node:assert` + relative imports, no node_modules), so +# CI runs them on a clean checkout with no `bench:install` — that's why they're in +# the core `check` gate. deno task test:deno +# The canonical-oracle test (NOT gated — it needs prettier/svelte, so run after +# `bench:install`): asserts the prettier baseline formats with a filepath, so `.ts` +# single-type-param arrows stay `` and `.svelte` ones get ``. +deno task test:deno:canonical + # Per-pattern corpus coverage with sample diffs (spot-check for overmatching) deno task corpus:compare:format --all --audit-patterns ``` @@ -314,12 +323,16 @@ JSON-only. ```bash deno task bench:install # one-time: install harness npm deps (see Cross-Runtime above) -# Run benchmarks (builds the runtime's bench artifacts automatically) -deno task bench # ALL three runtimes + compose → report.{deno,node,bun}.* + combined report.{json,md} -deno task bench:deno # Deno only -deno task bench:node # Node only -deno task bench:bun # Bun only (reuses the Node artifacts — N-API + nodejs-target WASM) -deno task bench:compose # Fold existing report.{deno,node,bun}.json → combined report.{json,md} +# Run benchmarks (builds the runtime's bench artifacts automatically). +# `bench` runs all three and FAILS FAST if node or bun isn't installed (the `&&` +# chain stops at the missing binary). Deno is the only hard dependency, so if you +# don't have node and/or bun, run the per-runtime tasks you DO have — each writes +# its own report..* sibling, and `bench:compose` folds whatever exists. +deno task bench # ALL three + compose (needs node AND bun installed) +deno task bench:deno # Deno only (no node/bun needed) +deno task bench:node # Node only (needs node) +deno task bench:bun # Bun only (needs bun; reuses the Node artifacts — N-API + nodejs-target WASM) +deno task bench:compose # Fold whatever report.{deno,node,bun}.json exist → combined report.{json,md} # Run without rebuilding (if already built) — guarded against stale artifacts, # see "Artifact Freshness Guard" below @@ -575,7 +588,7 @@ and `skip` overrides. Non-existent paths are silently skipped. benches/js/ ├── package.json # npm dep source of truth (both runtimes); install_deps drives it ├── package-lock.json # npm lock (committed for reproducibility) -├── deno.json # nodeModulesDir: manual + jsr-only imports (@std/fs) + fmt +├── deno.json # nodeModulesDir: manual + lock: false (npm from package.json; no jsr/remote deps) ├── install_deps.ts # `bench:install`: npm install + force-fetch the oxc wasi binding ├── bench.ts # Benchmark entry point (runtime-neutral — runs under Deno AND Node) ├── smoke.ts # Smoke test for formatters and parsers (runtime-neutral: smoke / smoke:node / smoke:bun) @@ -628,12 +641,11 @@ dep versions (the single source of truth, consumed by both runtimes) and `package-lock.json` pins their integrity. **Run `deno task bench:install`** to populate `node_modules` (one installer — `npm install` — plus the force-fetch of the oxc wasi binding; see [Cross-Runtime](#cross-runtime-deno--node--bun)). Deno reads -that `node_modules` via `"nodeModulesDir": "manual"`; Node reads it directly. -Only the jsr-only `@std/fs` (Deno-only diagnostics) stays in the `deno.json` -import map, locked by `deno.lock`. The Rust artifacts the bench builds (`tsv_ffi`, -`tsv_napi`, `tsv_wasm`) are pinned via `Cargo.lock`. Upgrading is always a -deliberate, committed act. A plain `npm install` prunes the oxc wasi binding — -re-run `bench:install`. +that `node_modules` via `"nodeModulesDir": "manual"`; Node reads it directly (the +config shape — no jsr/remote deps, no `deno.lock` — is covered above). +The Rust artifacts the bench builds (`tsv_ffi`, `tsv_napi`, `tsv_wasm`) are pinned +via `Cargo.lock`. Upgrading is always a deliberate, committed act. A plain +`npm install` prunes the oxc wasi binding — re-run `bench:install`. **Routine refresh** (alternative impls + infra — no fixture impact): @@ -642,8 +654,8 @@ cd benches/js && npm outdated # shows current vs latest # bump the version in benches/js/package.json, then: deno task bench:install # re-install at the new pins (+ re-fetch the oxc wasi binding) deno task smoke # confirm every impl still loads + formats (32 checks) -deno task bench # regenerate report.deno.* + report.node.* -# commit package.json + package-lock.json + deno.lock + results/report.* +deno task bench # regenerate report.{deno,node,bun}.* + combined report.{json,md} +# commit package.json + package-lock.json + results/report.* ``` These packages are free to bump independently — they're measured against, not @@ -688,7 +700,10 @@ corpus divergences against `@ryanatkn` code that tsv was formatting correctly. - oxc-parser (NAPI) — Fast TypeScript parser; languages: TypeScript, JS - oxfmt (NAPI) — Fast formatter; languages: TypeScript, JS, CSS, Svelte (experimental) -- biome (WASM) — Formatter/linter; languages: Svelte, TypeScript, JS, CSS +- biome (WASM) — Formatter/linter; languages: TypeScript, JS, CSS, and Svelte + (Svelte via biome's experimental HTML-superset support — `html.experimentalFullSupportEnabled`; + it formats the template **and** the embedded `