diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 19440b0..d8e5e7d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -7,7 +7,7 @@ on: push: branches: [main] pull_request: - branches: ['**'] + branches: ["**"] # Allow manually re-running the check from the Actions tab. workflow_dispatch: @@ -28,7 +28,7 @@ jobs: strategy: matrix: - node-version: ['24.14'] + node-version: ["24.14"] steps: # persist-credentials: false keeps the GITHUB_TOKEN out of .git/config, so a diff --git a/CLAUDE.md b/CLAUDE.md index 46c2c85..05b5e1e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,19 +23,21 @@ IMPORTANT for AI agents: Do NOT run `gro dev` - the developer will manage the de - Svelte 5 - component framework with runes - SvelteKit - application framework with static adapter -- fuz_css (`@fuzdev/fuz_css`) - CSS framework and design system +- fuz_css (`@fuzdev/fuz_css`) - semantic-first CSS framework and design system - fuz_ui (`@fuzdev/fuz_ui`) - UI components, theming, docs system - fuz_util (`@fuzdev/fuz_util`) - utility functions - fuz_code (`@fuzdev/fuz_code`) - syntax highlighting - Gro (`@fuzdev/gro`) - build system and task runner -- prettier + prettier-plugin-svelte - code formatting +- tsv (via Gro) - code formatting - mdz (`@fuzdev/mdz`) - markdown preprocessor wired into `svelte.config.js` - `@fuzdev/tsv_wasm` - tsv's formatter + parser as WASM; powers the playground, loaded lazily in the browser -Note: `@fuzdev/tsv_wasm` is loaded only on `/docs/playground` via a browser-only dynamic `import()`, so the ~900KB WASM never weighs down `/docs` or the prerendered pages. +Note: `@fuzdev/tsv_wasm` is loaded only on `/docs/playground` via a browser-only dynamic `import()`, so the ~900KB-gzipped WASM (~2.9MB decoded) never weighs down `/docs` or the prerendered pages. Note: several devDependencies — `@webref/css` (CSS spec data), `zimmerframe` (AST traversal), `@sveltejs/acorn-typescript`, `zod`, and `@fuzdev/blake3_wasm` — are *optional peer dependencies* of `@fuzdev/fuz_css`'s `vite_plugin_fuz_css`, declared here so its build-time CSS generation resolves them (e.g. `css_literal.ts` imports `@webref/css`, `css_class_extractor.ts` walks ASTs with `zimmerframe`). They aren't imported by this app's own source, so don't mistake them for dead deps. +Note: `vite` is deliberately held at 7.x (with `@sveltejs/vite-plugin-svelte` 6.x) — vite 8 + plugin-svelte 7 was buggy with this app or SvelteKit's integration. Don't upgrade to vite 8 without deliberately re-verifying the site works. + ## Scope tsv.fuz.dev is the public face of the tsv tool: @@ -56,8 +58,6 @@ tsv.fuz.dev is the public face of the tsv tool: ``` src/ -├── lib/ -│ └── sample.ts # placeholder constant (not yet wired up) ├── routes/ │ ├── +page.svelte # Home: hero, links to docs and benchmarks │ ├── +layout.svelte # Root layout: fuz_css/fuz_code CSS, ThemeRoot, SiteState @@ -81,23 +81,66 @@ src/ Benchmark data comes from `tsv`. Full workflow to update: ```bash -# 1. In ~/dev/tsv — run benchmarks (builds ffi + wasm automatically) +# 1. In ~/dev/tsv — run benchmarks across deno/node/bun (builds artifacts automatically) deno task bench -# 2. In ~/dev/tsv.fuz.dev — copy the latest result +# 2. In ~/dev/tsv.fuz.dev — copy the latest results npm run update-benchmarks ``` -Step 1 writes `benches/deno/results/report.json` (committed to tsv). -Step 2 copies it to `src/routes/docs/benchmarks/benchmarks.json`. -The JSON format matches `BenchmarkBaseline` in `benchmark_data.ts`. +Step 1 writes the per-runtime `benches/js/results/report..{json,md}` +siblings, the composed cross-runtime `report.{json,md}`, and the conformance +coverage report `report.conformance.node.json` (committed to tsv). +Step 2 copies three of them — `report.node.json` → `benchmarks.json`, the +composed `report.json` → `benchmarks_cross_runtime.json`, and +`report.conformance.node.json` → `benchmarks_conformance.json` — then runs +`gro format` over the copies. Note the script's source paths are hardcoded to +`../tsv` — if the reports were generated in a different worktree, copy them +into `~/dev/tsv` (or copy manually) first. +The JSON formats match the types in `benchmark_data.ts`. + +The end-to-end CLI comparison against Prettier, Biome, and Oxfmt comes from a +separate harness, a fork of Oxc's `bench-formatter` that adds tsv +(../oxc-bench-formatter). It publishes no JSON — its numbers live only as the +hyperfine console dump in its README, between the +`` / `END` markers, plus a `## Versions` list +and a `_Measured on: …_` machine line. To update: + +```bash +# 1. In ~/dev/oxc-bench-formatter — re-run and rewrite its README (needs a built ../tsv) +pnpm run update-readme + +# 2. In ~/dev/tsv.fuz.dev — reparse the README into JSON +gro gen +``` + +`benchmarks_formatters.gen.json.ts` parses that README and writes +`benchmarks_formatters.json`, keeping only the scenarios tsv participates in +(it has no JSX/TSX parser, so the harness runs it on the JSX-free corpora +only). + +A **missing** sibling checkout is the one tolerated case — generation is +skipped, the committed JSON stands, and `gro gen --check` passes on any machine +or CI that has only this repo (CI never checks out the harness, so it always +takes this path; no `--no-gen` needed). A README that **is** present but has +drifted fails the task loudly, naming the scenario and the section that stopped +parsing, rather than publishing stale or scenario-stripped numbers. A drift that +still parses but renames a scenario is caught on the site side instead: every +key in `benchmarks_cli.ts`'s `SCENARIO_COPY` must resolve to generated data, and +a test asserts it. Key files in `src/routes/docs/benchmarks/`: -- `benchmarks.json` — raw benchmark data (copied from tsv) +- `benchmarks.json` — per-runtime Node report (copied from tsv) +- `benchmarks_cross_runtime.json` — composed cross-runtime report (copied from tsv) +- `benchmarks_conformance.json` — conformance parse-coverage report (copied from tsv) +- `benchmarks_formatters.json` — formatter CLI comparison, generated from the sibling harness's README - `benchmark_data.ts` — TypeScript types matching the JSON format -- `benchmarks.ts` — re-exports the JSON with types +- `formatter_benchmark_data.ts` — types plus `parse_formatter_benchmarks`, the README parser +- `benchmarks_cli.ts` — shapes `benchmarks_formatters.json` for `BenchmarksCli.svelte` and owns the per-scenario prose; the numbers are all generated +- `benchmarks.ts`, `benchmarks_cross_runtime.ts`, `benchmarks_conformance.ts`, `benchmarks_formatters.ts` — re-export the JSON with types - `BenchmarksBar.svelte`, `BenchmarksGroup.svelte`, etc. — visualization components +- `BenchmarksBaselineGroup.svelte` — shared interactive column behind the format, parse, and binary-size groups: hovering a row re-baselines that group's ratios (each of the three groups per section is independent), restoring the default anchor (the canonical reference — Prettier for format, the JS baseline for parse — and the smallest build for size) on mouseleave. `benchmark_data.ts`'s `compute_baseline_ratio`/`format_baseline_ratio`/`baseline_ratio_color` carry the per-`BaselineDirection` (`speed`/`size`) formulas it and the derivations share ## Architecture @@ -105,6 +148,7 @@ Key files in `src/routes/docs/benchmarks/`: - Uses fuz_ui tome system for docs navigation - `docs/tomes.ts` defines the doc sections: introduction, playground, benchmarks - Benchmark data lives in `src/routes/docs/benchmarks/benchmarks.json` (see [Benchmarks](#benchmarks)) +- The benchmarks page quotes no hand-written ratios — its prose computes them from the same reports the charts render, via `benchmark_data.ts`'s `benchmark_speedup`/`format_ratio_approx`/`format_ratio_range` and `benchmarks_cli.ts`'s `cli_speedup_vs_tsv`/`cli_memory_ratio_range`. A test gates that every pair the copy names still resolves - `library.ts` builds component metadata at runtime from the `virtual:svelte-docinfo` module (provided by the `svelte-docinfo` Vite plugin); the docs index passes it to `DocsContent` - The playground (`/docs/playground`) loads `@fuzdev/tsv_wasm` via a browser-only dynamic `import()` inside `Playground.svelte`, so the WASM code-splits into its own chunk fetched only on that route — the same lazy discipline `library.ts` uses for the heavy svelte-docinfo data, keeping `/docs` and the prerendered pages WASM-free. `@fuzdev/tsv_wasm` is in `vite.config.ts` `optimizeDeps.exclude` (like `blake3_wasm`) - The playground's editor is fuz_code's `CodeTextarea` (live syntax highlighting via the experimental CSS Custom Highlight API). It needs `@fuzdev/fuz_code/theme_highlight.css`, imported inside `Playground.svelte` rather than the root layout so it stays on this route only; `supports_css_highlight_api()` drives a graceful-degradation note where the API is unavailable (the editor still works, unstyled) @@ -117,7 +161,7 @@ Deploys to `https://tsv.fuz.dev/` via `gro deploy` (builds and pushes to deploy - TypeScript strict mode - Svelte 5 with runes API -- Prettier with tabs, 100 char width +- tsv with tabs, 100 char width - Node >= 24.14 - Private package (not published to npm) diff --git a/package-lock.json b/package-lock.json index ee9152a..f1da569 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,31 +14,28 @@ "@fuzdev/fuz_css": "^0.63.2", "@fuzdev/fuz_ui": "^0.206.5", "@fuzdev/fuz_util": "^0.65.2", - "@fuzdev/gro": "^0.205.1", + "@fuzdev/gro": "^0.206.0", "@fuzdev/mdz": "^0.2.0", "@fuzdev/tsv_wasm": "^0.1.0", "@ryanatkn/eslint-config": "^0.12.1", "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.61.1", - "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@sveltejs/kit": "^2.69.1", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/estree": "^1.0.8", "@types/node": "^24.12.4", "@webref/css": "^8.2.0", "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.1", "esm-env": "^1.2.2", - "magic-string": "^0.30.21", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.56.0", - "svelte-check": "^4.3.6", + "svelte": "^5.56.4", + "svelte-check": "^4.7.2", "svelte-docinfo": "^0.5.4", "svelte2tsx": "^0.7.47", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", - "vite": "^8.0.16", + "vite": "^7.3.6", "vitest": "^4.1.9", "zimmerframe": "^1.1.4", "zod": "^4.3.6" @@ -54,6 +51,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -66,6 +64,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -77,6 +76,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -94,7 +94,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -112,7 +111,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -130,7 +128,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -148,7 +145,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -166,7 +162,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -184,7 +179,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -202,7 +196,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -220,7 +213,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -238,7 +230,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -256,7 +247,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -274,7 +264,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -292,7 +281,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -310,7 +298,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -328,7 +315,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -346,7 +332,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -364,7 +349,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -382,7 +366,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -400,7 +383,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -418,7 +400,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -436,7 +417,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -454,7 +434,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -472,7 +451,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -490,7 +468,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -508,7 +485,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -526,7 +502,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -544,7 +519,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -884,18 +858,17 @@ } }, "node_modules/@fuzdev/gro": { - "version": "0.205.1", - "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.205.1.tgz", - "integrity": "sha512-o5WBXWSH/UlN1BsDnpBNIi0WHR7lj7UFGltH/JctSP7oSB2ojvmoXD5OLCEeVdRVcDaSQKl48xcXKUwHKqVSmQ==", + "version": "0.206.0", + "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.206.0.tgz", + "integrity": "sha512-11nkTAEWDOvkgzynRChQS+bj3tc7R0NduB+eHLXzXZJqz66nc/oop8Ttvz0UkjTBPX/zzgtU8QuDDNyA7buf5A==", "dev": true, "license": "MIT", "dependencies": { + "@fuzdev/tsv_wasm": "^0.1.0", "chokidar": "^5.0.0", "dotenv": "^17.2.3", "esm-env": "^1.2.2", "oxc-parser": "^0.99.0", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.5.1", "ts-blank-space": "^0.6.2", "tslib": "^2.8.1" }, @@ -1393,27 +1366,38 @@ "dev": true, "license": "MIT" }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -1422,15 +1406,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -1439,15 +1420,26 @@ "optional": true, "os": [ "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -1456,32 +1448,46 @@ "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], @@ -1493,15 +1499,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], @@ -1513,15 +1516,46 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], @@ -1533,17 +1567,31 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ - "s390x" + "riscv64" ], "dev": true, "libc": [ @@ -1553,17 +1601,31 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ - "x64" + "s390x" ], "dev": true, "libc": [ @@ -1573,15 +1635,29 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], @@ -1593,15 +1669,26 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -1610,51 +1697,40 @@ "optional": true, "os": [ "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ - "wasm32" + "arm64" ], "dev": true, "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" - } + "os": [ + "win32" + ] }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -1663,17 +1739,21 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@ryanatkn/eslint-config": { "version": "0.12.1", @@ -1724,9 +1804,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.61.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.61.1.tgz", - "integrity": "sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg==", + "version": "2.69.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.69.1.tgz", + "integrity": "sha512-+nz8Fx/cElzb2ZPHC+6Ll3y3NEVIe4Na75PeplLlyTmd1dBXAjz2KR14y1ZgNjb2ThfAYzulu+PFy1UE3RCUzA==", "dev": true, "license": "MIT", "dependencies": { @@ -1765,24 +1845,53 @@ } } }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", - "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "dev": true, "license": "MIT", "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", - "vitefu": "^1.1.2" + "vitefu": "^1.1.1" }, "engines": { "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "svelte": "^5.46.4", - "vite": "^8.0.0-beta.7 || ^8.0.0" + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "obug": "^2.1.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@tybys/wasm-util": { @@ -1822,9 +1931,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -2545,16 +2654,6 @@ "node": ">=0.10.0" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/devalue": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", @@ -2589,7 +2688,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -2801,9 +2899,9 @@ } }, "node_modules/esrap": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", - "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz", + "integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==", "dev": true, "license": "MIT", "dependencies": { @@ -3181,279 +3279,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -3879,33 +3704,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", - "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", - "dev": true, - "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": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", - "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3940,48 +3738,49 @@ "node": ">=4" } }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" + "@types/estree": "1.0.9" }, "bin": { - "rolldown": "bin/cli.mjs" + "rollup": "dist/bin/rollup" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } - }, - "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" } }, "node_modules/sade": { @@ -4120,9 +3919,9 @@ } }, "node_modules/svelte": { - "version": "5.56.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.0.tgz", - "integrity": "sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg==", + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", + "integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==", "dev": true, "license": "MIT", "dependencies": { @@ -4137,7 +3936,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.9", + "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -4148,13 +3947,14 @@ } }, "node_modules/svelte-check": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.6.tgz", - "integrity": "sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.2.tgz", + "integrity": "sha512-GoS4XJdGswlq0rIT1vtFLzJY1bvHtY37McY9H9Gkm1Ggw/ICdZYn8J/Z8Yi0BEL0i3R4+jtaWVePjyppMlij/A==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", @@ -4432,17 +4232,18 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" @@ -4458,10 +4259,9 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", + "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -4474,18 +4274,15 @@ "@types/node": { "optional": true }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, "jiti": { "optional": true }, "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, diff --git a/package.json b/package.json index e1c6a86..3f5f169 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test": "gro test", "preview": "vite preview", "deploy": "gro deploy", - "update-benchmarks": "cp ../tsv/benches/deno/results/report.json src/routes/docs/benchmarks/benchmarks.json" + "update-benchmarks": "cp ../tsv/benches/js/results/report.node.json src/routes/docs/benchmarks/benchmarks.json && cp ../tsv/benches/js/results/report.json src/routes/docs/benchmarks/benchmarks_cross_runtime.json && cp ../tsv/benches/js/results/report.conformance.node.json src/routes/docs/benchmarks/benchmarks_conformance.json && gro format" }, "type": "module", "engines": { @@ -24,56 +24,36 @@ }, "devDependencies": { "@fuzdev/blake3_wasm": "^0.1.1", - "@fuzdev/fuz_code": "^0.46.3", + "@fuzdev/fuz_code": "^0.48.0", "@fuzdev/fuz_css": "^0.63.2", - "@fuzdev/fuz_ui": "^0.206.5", + "@fuzdev/fuz_ui": "^0.206.7", "@fuzdev/fuz_util": "^0.65.2", - "@fuzdev/gro": "^0.205.1", - "@fuzdev/mdz": "^0.2.0", - "@fuzdev/tsv_wasm": "^0.1.0", - "@ryanatkn/eslint-config": "^0.12.1", + "@fuzdev/gro": "^0.208.0", + "@fuzdev/mdz": "^0.3.0", + "@fuzdev/tsv_wasm": "^0.2.0", + "@ryanatkn/eslint-config": "^0.12.2", "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.61.1", - "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@sveltejs/kit": "^2.69.1", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/estree": "^1.0.8", "@types/node": "^24.12.4", "@webref/css": "^8.2.0", "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.1", "esm-env": "^1.2.2", - "magic-string": "^0.30.21", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.56.0", - "svelte-check": "^4.3.6", + "svelte": "^5.56.4", + "svelte-check": "^4.7.2", "svelte-docinfo": "^0.5.4", "svelte2tsx": "^0.7.47", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", - "vite": "^8.0.16", + "vite": "^7.3.6", "vitest": "^4.1.9", "zimmerframe": "^1.1.4", "zod": "^4.3.6" }, - "prettier": { - "plugins": [ - "prettier-plugin-svelte" - ], - "useTabs": true, - "printWidth": 100, - "singleQuote": true, - "bracketSpacing": false, - "overrides": [ - { - "files": "package.json", - "options": { - "useTabs": false - } - } - ] - }, "sideEffects": [ "**/*.css" ], diff --git a/src/app.d.ts b/src/app.d.ts index 8364d73..b9e1d38 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -6,7 +6,7 @@ declare module 'virtual:fuz.css' { export default css; } declare module 'virtual:pkg.json' { - import type {PkgJson} from '@fuzdev/fuz_util/pkg_json.ts'; + import type { PkgJson } from '@fuzdev/fuz_util/pkg_json.ts'; const pkg_json: PkgJson; export default pkg_json; } diff --git a/src/app.html b/src/app.html index e3061bc..a1c58fd 100644 --- a/src/app.html +++ b/src/app.html @@ -1,23 +1,29 @@ - - - - - + + + + + - + - %sveltekit.head% - - -
%sveltekit.body%
- + %sveltekit.head% + + +
%sveltekit.body%
+ diff --git a/src/lib/sample.ts b/src/lib/sample.ts deleted file mode 100644 index 429c922..0000000 --- a/src/lib/sample.ts +++ /dev/null @@ -1 +0,0 @@ -export const sample = 'TODO - publish tsv!'; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1e580bb..6411db4 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,13 +4,13 @@ import './style.css'; import ThemeRoot from '@fuzdev/fuz_ui/ThemeRoot.svelte'; - import {SiteState, site_context} from '@fuzdev/fuz_ui/site.svelte.ts'; - import {logo_tsv} from '@fuzdev/fuz_ui/logos.ts'; - import type {Snippet} from 'svelte'; + import { SiteState, site_context } from '@fuzdev/fuz_ui/site.svelte.ts'; + import { logo_tsv } from '@fuzdev/fuz_ui/logos.ts'; + import type { Snippet } from 'svelte'; import pkg_json from 'virtual:pkg.json'; const { - children, + children }: { children: Snippet; } = $props(); @@ -19,7 +19,7 @@ // and `repo_url` points to the tsv tool repo rather than this website's repo // (`pkg_json.repository`). site_context.set( - new SiteState({icon: logo_tsv, pkg_json, repo_url: 'https://github.com/fuzdev/tsv'}), + new SiteState({ icon: logo_tsv, pkg_json, repo_url: 'https://github.com/fuzdev/tsv' }) ); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e98aa2e..13a24d8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,10 +1,10 @@ diff --git a/src/routes/docs/+layout.svelte b/src/routes/docs/+layout.svelte index a161ea3..28a2d09 100644 --- a/src/routes/docs/+layout.svelte +++ b/src/routes/docs/+layout.svelte @@ -1,13 +1,13 @@

- tsv is a toolchain for TypeScript/JS, CSS, and Svelte in Rust. Performance is its priority - after correctness, and the numbers compare favorably to Oxc and Biome for its supported - languages. + tsv is a toolchain for TypeScript/JS, CSS, and Svelte in Rust. After correctness, performance + is tsv's next priority. This page shows how it measures up against Prettier, which tsv closely + follows, and against Oxc and Biome, which are similar tools with wider language support (tsv + doesn't support JSX/TSX/SCSS/etc).

-
-
- tsv native - tsv native json - tsv wasm - tsv wasm json - biome - oxc - canonical -
-
- - +

- Rather than supporting many languages, tsv focuses on Svelte/HTML, TypeScript/JS, and CSS. - This lets it be smaller when it's all you need, a quality that's more relevant when used in - the browser via wasm: + Compared to Oxc and Biome, tsv is faster, smaller, and uses less memory to parse and format + its supported languages. This section has a prose summary; skip ahead for charts.

- +

+ The measurements on this page are single-threaded and in-process, where each tool parses or + formats one file at a time (isolating engine speed from multi-core parallelism), measured over + a large collection of real-world repos: {corpus_file_count.toLocaleString('en-US')} files including + Svelte's official repos (svelte, kit, svelte.dev) and the + fuz.dev repos. On that basis: +

+
    +
  • + Formatting TypeScript, tsv is ~{format_ts_vs_oxfmt} faster than Oxfmt (native-vs-native), ~{format_ts_vs_prettier} + faster than Prettier (native Rust vs Prettier's JS), and ~{format_ts_vs_biome} faster than Biome + (wasm-vs-wasm). +
  • +
  • + Formatting Svelte, tsv is is ~{format_svelte_vs_prettier} faster than Prettier (which Oxfmt's + Svelte path delegates to internally) and {format_svelte_vs_biome} faster than Biome. +
  • +
  • + Formatting CSS, it's ~{format_css_vs_oxfmt} faster than Oxfmt and {format_css_vs_biome} faster + than Biome. +
  • +
  • + Parsing TypeScript to JSON, tsv is ~{parse_ts_vs_oxc} faster than Oxc; Biome doesn't expose its + parser to JS. tsv's default AST adds a per-node line/column loc for drop-in Svelte + compatibility, with a fast path to reconstruct locs in JS. +
  • +
  • + A fork of Oxc's official bench-formatter + is an end-to-end CLI benchmark with its own corpus. On a real TypeScript repo, tsv formats ~{cli_ts_wall_vs_oxfmt} + faster than Oxfmt and ~{cli_ts_wall_vs_biome} faster than Biome using + {cli_ts_memory ? format_ratio_range(cli_ts_memory.min, cli_ts_memory.max) : '—'} less memory. + Wall-clock measurements are warped by each tool's multi-file parallelism, so they scale with core + count and are machine-dependent, and could change with tuning. +
  • +
+

+ Each section below has notes that attempt to fairly contextualize its numbers. The charts show + numbers using Node v24, and at the end of the page is a cross-runtime comparison. +

+
+ + + + + + + + +

+ tsv's formatter is similar to Oxfmt, Biome, and + dprint. It formats Svelte, TypeScript, + and CSS, plus JS as strict-mode TypeScript: +

+ {#each format_groups as group (group.language)} + + {/each}
- - + +

+ The parse entries that build a full JS AST are comparable in mechanism: tsv and oxc-parser + both serialize the AST to JSON in Rust and deserialize it in JS, native and wasm alike. But + the deliverables differ — tsv's default wire (tsv-json / + tsv_wasm-json) carries a per-node loc (line/column) object that + oxc-parser's default span-only AST omits, and that loc is roughly half the wire + bytes and most of its JSON.parse cost. The tsv-json-no-locations / + tsv_wasm-json-no-locations entries drop it, emitting the same span-only shape oxc does, + so those are the payload-matched, apples-to-apples comparison with oxc-parser (line/column stays + derivable from the offsets plus source, so nothing is lost). The tsv-internal and tsv_wasm-internal + entries build the native AST but skip JS-side materialization, so they show raw in-engine speed + rather than a cross-tool comparison. +

+ {#each parse_groups as group (group.language)} + + {/each} +
- +

- tsv's formatter is similar to Oxfmt - and Biome. Today it can format Svelte, TypeScript, and CSS, - plus HTML and JS (as strict-mode TypeScript): + Separate from the speed numbers above that use a real-world corpus of code, this section + measures parse coverage: how much of a much larger, deliberately hard corpus each + parser accepts — Prettier's format-test suites, Svelte's compiler test suite, CSS extracted + from + web-platform-tests, and + test262's expected-valid strict-mode tests.

- {#each format_groups as group (group.language)} - - {/each} + +
+ + + +

+ Rather than supporting many languages, tsv focuses on Svelte/HTML, TypeScript/JS, and CSS. + This lets it be smaller when it's all you need, a quality that's more relevant when used in + the browser via wasm. +

+ +
- +

- The parse rows that build a full JS AST are directly comparable: tsv and oxc-parser both - serialize the AST to JSON in Rust and deserialize it in JS, native and wasm alike. The - tsv-internal and tsv_wasm-internal rows are tsv's parse-only numbers - they build the native - AST but skip JS-side materialization, so they show raw in-engine speed rather than a - cross-tool comparison. + The numbers above measure tsv's engine in-process, one file at a time, using tsv's original + benchmarks. This section describes the results from a fork of Oxc's official + bench-formatter + that + adds tsv, timing + the whole CLI end-to-end — process spawn, file discovery, I/O, and each tool's default + multi-file parallelism — plus peak memory. It's the "what you experience typing the command" + measure, run on real repositories. tsv appears in the two JSX-free scenarios (it has no + JSX/TSX parser).

- {#each parse_groups as group (group.language)} - - {/each} +
@@ -152,39 +381,53 @@

All numbers are single-threaded: every library formats or parses one file at a time, measured - sequentially with no cross-file parallelism. These are per-file, single-core latency and - throughput numbers - not the multi-core batch throughput a CLI gets when it formats many files - at once, which most of these tools (tsv included) can do. + sequentially with no cross-file parallelism. Each row is the total time to process the whole + corpus once on a single core — not the multi-core batch throughput a CLI gets when it formats + many files at once, which most of these tools (tsv included) can do.

- What's measured: around 5,500 files (~15 MB) of - .svelte/.html, .ts/.js, and - .css, from three sources: the fuz.dev libraries and apps, upstream framework - source (Svelte, SvelteKit, and the svelte.dev site), and formatter conformance fixtures - (Prettier's and prettier-plugin-svelte's own test suites - deliberately tricky edge cases, not - typical code, so they skew the corpus toward hard cases). + What's measured: {corpus_file_count.toLocaleString('en-US')} files of .svelte, + .ts/.js, and .css — real-world code only, from two + sources: the author's libraries, apps, and sites (the fuz.dev ecosystem plus personal + SvelteKit sites), and upstream framework source (Svelte, SvelteKit, and the svelte.dev site). + The CSS set also includes real-authored CSS extracted from those components' + <style> blocks, concatenated per repo — standalone CSS files are rare in this + ecosystem, and the same bytes appear in the Svelte rows (rows are never summed). Test files count + as real code and stay in; fixture files (the formatter test suites that used to be part of this + corpus, and fixture subtrees inside the measured repos) are excluded — deliberately tricky edge + cases measure conformance, not typical throughput, and are covered by the parse-conformance section + above.

-
- + + +

+ The same benchmark harness runs under three JS runtimes — Node, Deno, and Bun. The headline + numbers above are the Node run. The native entry differs by runtime: Node and Bun load tsv's + N-API addon, while Deno loads its C-FFI library. They share code but cross a different binding + boundary, so a per-runtime delta on the same row is a runtime effect — the JS engine or the + binding boundary — not a difference in tsv's own engine, which is identical across all three. +

+ + +
+ diff --git a/src/routes/docs/benchmarks/BenchmarksBar.svelte b/src/routes/docs/benchmarks/BenchmarksBar.svelte index 128c7b5..258add9 100644 --- a/src/routes/docs/benchmarks/BenchmarksBar.svelte +++ b/src/routes/docs/benchmarks/BenchmarksBar.svelte @@ -3,7 +3,7 @@ category_color, format_label, type FormattedUnit, - type ImplementationCategory, + type ImplementationCategory } from './benchmark_data.ts'; const { @@ -14,6 +14,9 @@ ratio_text, ratio_color, annotation, + disabled = false, + on_enter, + on_leave }: { label: string; bar_fraction: number; @@ -24,48 +27,96 @@ // optional extra context shown between value and ratio (corpus coverage in // benchmark groups, gzipped size in binary-size groups); omitted when absent annotation?: string | undefined; + // a grayed-out, inert placeholder (a tool that doesn't run in this group) — + // no bar, no value, no ratio, just the label held in its shared slot + disabled?: boolean; + // hover-to-rebaseline: fired when the pointer enters/leaves the row so the + // group can adopt this row as its ratio anchor; omitted on inert placeholders + on_enter?: (() => void) | undefined; + on_leave?: (() => void) | undefined; } = $props(); + + // the parenthesized binding suffix (`(wasm)`/`(node napi)`) describes how the + // tool actually ran in this (Node) report - meaningless for the disabled + // placeholders of single-variant tools (biome and dprint each ship only a wasm + // build here, so there's no ambiguity to lose). oxc-parser keeps its suffix even + // when disabled since it has two placeholder rows (napi and wasm) that would + // otherwise become indistinguishable. + const display_label = $derived( + disabled && (category === 'biome' || category === 'dprint') + ? format_label(label).replace(/ \([^)]*\)$/, '') + : format_label(label) + ); -
- {format_label(label)} + + +
+ {display_label}
-
+ {#if !disabled} +
+ {/if}
- {value.value} {value.unit} + + {#if disabled} + n/a + {:else} + {value.value} {value.unit} + {/if} + {#if annotation != null} {annotation} {/if} - {ratio_text} + {#if !disabled}{ratio_text}{/if}
diff --git a/src/routes/docs/benchmarks/BenchmarksConformance.svelte b/src/routes/docs/benchmarks/BenchmarksConformance.svelte new file mode 100644 index 0000000..87a21f0 --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksConformance.svelte @@ -0,0 +1,76 @@ + + +{#each groups as group (group.language)} +
+

+ Parsing {group.files_total.toLocaleString('en-US')} + {LANGUAGE_LABELS[group.language] ?? group.language} files +

+ + + + + + + + + + {#each group.rows as row (row.name)} + + + + + + {/each} + +
files acceptedcoverage
{row.name}{row.files_processed.toLocaleString('en-US')} / {row.files_total.toLocaleString( + 'en-US' + )}{format_coverage_percent(row.coverage_fraction)}
+
+{/each} + + diff --git a/src/routes/docs/benchmarks/BenchmarksCrossRuntime.svelte b/src/routes/docs/benchmarks/BenchmarksCrossRuntime.svelte new file mode 100644 index 0000000..c71ac86 --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksCrossRuntime.svelte @@ -0,0 +1,146 @@ + + +
+ {#if report.mixed_vintage} + + {/if} + {#if report.mixed_machine} + + {/if} + {#if runtime_versions.length} +
    + {#each runtime_versions as { runtime, version } (runtime)} +
  • {runtime} {version}
  • + {/each} +
+ {/if} + {#each groups as group (group.group)} +
+

{group_label(group.operation, group.language)}

+ + + + + {#each runtimes as runtime (runtime)} + + {/each} + {#each others as runtime (runtime)} + + {/each} + + + + {#each group.rows as row (row.name)} + + + {#each runtimes as runtime (runtime)} + + {/each} + {#each others as runtime (runtime)} + {@const ratio = row.ratio_vs_base[runtime]} + + {/each} + + {/each} + +
{runtime}{runtime}/{base}
+ + {format_cross_runtime_label(row.name)} + {format_ops(row.ops_per_second[runtime])} + {ratio != null ? format_speedup(ratio) : 'fail'} +
+
+ {/each} +

+ sweeps/sec — one sweep is a full pass over the group's timed file set (higher is faster); ratios + are vs {base} (> 1 = faster than {base}). A + fail is an implementation that runtime can't load (see notes above). The + native rows load each runtime's idiomatic binding of the same engine — the N-API + addon under node and bun (tsv (node napi)), the C-FFI + library under deno (tsv (deno ffi)) — so the deno column is + a first-class FFI-vs-N-API comparison, not a re-run of the same binding. +

+
+ + diff --git a/src/routes/docs/benchmarks/BenchmarksGroup.svelte b/src/routes/docs/benchmarks/BenchmarksGroup.svelte index d3e2cb8..585348d 100644 --- a/src/routes/docs/benchmarks/BenchmarksGroup.svelte +++ b/src/routes/docs/benchmarks/BenchmarksGroup.svelte @@ -1,30 +1,33 @@ @@ -35,25 +38,7 @@ {count_label} {group.language} files - {#if has_coverage}files handled / total  ·  speed{/if} + total time  ·  speed

-
- {#each group.entries as entry (entry.name)} - - {/each} -
+
diff --git a/src/routes/docs/benchmarks/BenchmarksMeta.svelte b/src/routes/docs/benchmarks/BenchmarksMeta.svelte index 596ee77..66eaa65 100644 --- a/src/routes/docs/benchmarks/BenchmarksMeta.svelte +++ b/src/routes/docs/benchmarks/BenchmarksMeta.svelte @@ -1,22 +1,24 @@ -{#if wasm_display.length > 0} -
-

wasm

-
- {#each wasm_display as s (s.label)} - - {/each} -
-
-{/if} - -{#if native_display.length > 0} +{#each size_groups as group (group.capability)}
-

native

-
- {#each native_display as s (s.label)} - - {/each} -
+

{group.heading}

+
-{/if} +{/each} diff --git a/src/routes/docs/benchmarks/benchmark_data.ts b/src/routes/docs/benchmarks/benchmark_data.ts index 9bf0958..4add8af 100644 --- a/src/routes/docs/benchmarks/benchmark_data.ts +++ b/src/routes/docs/benchmarks/benchmark_data.ts @@ -1,7 +1,12 @@ -// Raw types matching the tsv bench.ts `Baseline` format +// Raw types matching the tsv bench's per-runtime report format +// (`benches/js/results/report..json` — the site's flagship detailed +// view is the Node report, the N-API native path) export interface BenchmarkBaseline { version: number; + // The runtime that produced this report (`node` for the flagship view). + // Present from report `version` 5 on. + runtime?: string; timestamp: string; git_commit: string; corpus: Record; @@ -11,23 +16,91 @@ export interface BenchmarkBaseline { // Counts of silenced third-party stderr noise, keyed by message pattern. // Present from baseline `version` 4 on; not rendered, kept for parity. suppressed_noise?: Record; + // Which corpus/surface produced the report: `perf` (real-world corpus, + // format + parse) or `conformance` (the deliberately-hard fixture suites, + // disjoint from the perf corpus, parse only). Present from `version` 6 on. + corpus_kind?: 'perf' | 'conformance'; + // Per-entry corpus composition (path + loaded file count) — discloses which + // sources were present on the machine that produced the report. Present + // from `version` 6 on. + corpus_sources?: Array; + // The machine that produced the report — CPU model, OS/arch, runtime version. + // The throughput numbers are machine-relative, so this is the environment the + // meta panel discloses. Present from `version` 7 on (absent on older reports). + machine?: Machine; } +// The hardware/runtime a report was measured on. Excludes hostname (the reports +// are published) and volatile fields (free memory, load) that would churn. +export interface Machine { + cpu_model: string; + os: string; + arch: string; + // The producing runtime's own version (`node`/`deno`/`bun` version string). + runtime_version: string; +} + +export interface CorpusSource { + path: string; + files: number; + // Per-language split of `files` (svelte/typescript/css counts summing to + // `files`). Present on reports whose loader emitted it; older reports carry + // only the `files` total, so treat it as optional. + by_language?: Partial>; + // The source's GitHub origin, git-detected by the bench at report-build time + // (URL + commit + subpath). Present from `version` 8 on; absent on older + // reports and on sources with no GitHub remote — treat as optional. + repo?: CorpusRepoRef; +} + +// A corpus source's GitHub origin — see `CorpusSource.repo` and `corpus_source_url`. +export interface CorpusRepoRef { + // Canonical https GitHub URL, e.g. `https://github.com/sveltejs/svelte`. + url: string; + // `owner/name` (e.g. `sveltejs/svelte`) — a compact label. + slug: string; + // The commit the corpus was loaded at (full SHA); `''` for a harvested cache + // linked at its canonical upstream root (no pin). + commit: string; + // Path within the repo to this source (`''` = repo root). + subpath: string; +} + +/** + * The GitHub URL for a corpus source, pinned to the measured commit + subpath + * (`…/tree//`) when detected, or the repo root for a + * canonical-upstream cache (empty `commit`). `undefined` when the source has no + * detected origin (older reports, or the local `svelte_styles` cache). + */ +export const corpus_source_url = (source: CorpusSource): string | undefined => { + const repo = source.repo; + if (!repo) return undefined; + if (!repo.commit) return repo.url; + return repo.subpath + ? `${repo.url}/tree/${repo.commit}/${repo.subpath}` + : `${repo.url}/tree/${repo.commit}`; +}; + export interface BaselineEntry { name: string; group: string; - mean_ns: number; - p50_ns: number; - p75_ns: number; - p90_ns: number; - p95_ns: number; - p99_ns: number; - min_ns: number; - max_ns: number; - std_dev_ns: number; - cv: number; - ops_per_second: number; - sample_size: number; + // Timing stats are `null` on a coverage-only report (the conformance surface + // the site refreshes from — parse coverage measured, timed phase skipped). A + // perf report always carries real numbers, and only the perf path + // (`derive_benchmark_groups`) reads these, so the nulls are unreachable there + // but must be expressed for the shared cast to stay sound. + mean_ns: number | null; + p50_ns: number | null; + p75_ns: number | null; + p90_ns: number | null; + p95_ns: number | null; + p99_ns: number | null; + min_ns: number | null; + max_ns: number | null; + std_dev_ns: number | null; + cv: number | null; + ops_per_second: number | null; + sample_size: number | null; // Per-implementation preflight coverage: files this impl processed / the // language's total discovered files. Present from baseline `version` 3 on; // absent (or `null`) in older baselines. @@ -36,6 +109,9 @@ export interface BaselineEntry { // Files this impl was actually timed on (the per-group intersection in // default mode). Present from baseline `version` 4 on. files_iterated?: number | null; + // Present from report `version` 5 on (matches the report's top-level); + // not rendered, kept for parity. + runtime?: string; } export interface BaselineVersions { @@ -48,6 +124,9 @@ export interface BaselineVersions { oxc_parser?: string; oxfmt?: string; biome?: string; + // `@dprint/typescript` — the plugin version (the host `@dprint/formatter` is + // just the Wasm loader). Absent on reports produced before the dprint row. + dprint?: string; } export interface BinarySize { @@ -68,6 +147,7 @@ export type ImplementationCategory = | 'tsv_wasm' | 'tsv_wasm_json' | 'biome' + | 'dprint' | 'oxc'; export interface BenchmarkGroup { @@ -84,10 +164,14 @@ export interface BenchmarkDisplayEntry { name: string; mean_ns: number; bar_fraction: number; - speedup_vs_canonical: number | undefined; category: ImplementationCategory; files_processed: number | null; files_total: number | null; + // A placeholder entry mirrored from another language's group for a tool that + // doesn't run in this one (e.g. `oxc-parser` under svelte/css parse) — rendered + // grayed-out and inert so the parse groups share one entry order. Absent on real, + // measured entries. + disabled?: boolean; } export interface SpeedupRow { @@ -105,35 +189,29 @@ const CATEGORY_BY_NAME: Record = { 'acorn-typescript': 'canonical', tsv: 'tsv_native', 'tsv-json': 'tsv_native_json', + 'tsv-json-no-locations': 'tsv_native_json', 'tsv-internal': 'tsv_native', tsv_wasm: 'tsv_wasm', 'tsv_wasm-json': 'tsv_wasm_json', + 'tsv_wasm-json-no-locations': 'tsv_wasm_json', 'tsv_wasm-internal': 'tsv_wasm', 'biome-wasm': 'biome', + 'dprint-wasm': 'dprint', 'oxc-parser': 'oxc', 'oxc-parser-wasm': 'oxc', - oxfmt: 'oxc', + oxfmt: 'oxc' }; -const categorize_name = (name: string): ImplementationCategory => CATEGORY_BY_NAME[name] ?? 'oxc'; +export const categorize_name = (name: string): ImplementationCategory => + CATEGORY_BY_NAME[name] ?? 'oxc'; export const categorize_size = (label: string): ImplementationCategory => { // covers `tsv_wasm` plus the `tsv_format_wasm`/`tsv_parse_wasm` subsets if (label.startsWith('tsv') && label.includes('wasm')) return 'tsv_wasm'; if (label.startsWith('tsv')) return 'tsv_native'; if (label.startsWith('biome')) return 'biome'; - if (label.includes('oxc') || label.includes('oxfmt')) return 'oxc'; - return 'oxc'; -}; - -// Canonical entry names per group -const CANONICAL_BY_GROUP: Record = { - 'parse/svelte': 'svelte/compiler', - 'parse/typescript': 'acorn-typescript', - 'parse/css': 'svelte/compiler', - 'format/svelte': 'prettier', - 'format/typescript': 'prettier', - 'format/css': 'prettier', + if (label.startsWith('dprint')) return 'dprint'; + return 'oxc'; // oxc-parser / oxfmt, and any unrecognized label }; // Primary tsv entry names for speedup summary (fair comparisons) @@ -142,6 +220,48 @@ const PRIMARY_WASM_FORMAT = 'tsv_wasm'; // Derivation functions +// Shared display order for benchmark groups: format before parse, then by +// language — used by the detailed, conformance, and cross-runtime views alike. +const OPERATION_ORDER: Record = { format: 0, parse: 1 }; +const LANGUAGE_ORDER: Record = { + svelte: 0, + typescript: 1, + css: 2 +}; + +/** + * Fixed slot for a format/parse row, applied in place of a size-ordered sort so the + * rows read in a stable, meaningful sequence across every group: the canonical + * reference first (the default 1.0x anchor), then the cross-tool comparisons + * (alphabetically: biome, dprint, then oxc), then tsv's JSON-materializing wires (the + * span-only `no-locations` wire before the default `loc`-carrying one), then tsv's raw + * internal engine. + */ +const speed_entry_rank = (entry: BenchmarkDisplayEntry): number => { + if (entry.category === 'canonical') return 0; + if (entry.category === 'biome') return 1; + if (entry.category === 'dprint') return 2; + if (entry.category === 'oxc') return 3; + if (entry.name.endsWith('-no-locations')) return 4; // tsv json, span-only wire + if (entry.name.endsWith('-json')) return 5; // tsv json, loc-carrying wire + return 6; // tsv-internal / tsv_wasm-internal — raw in-engine, no JS materialization +}; + +/** + * Orders format/parse rows by their fixed `speed_entry_rank` slot, then wasm before + * native within a tier (the browser-relevant build leads each pairing), then by name + * — shared by the initial sort and the re-sort after disabled placeholders are mixed + * in, so every group scans identically. + */ +const compare_speed_entries = (a: BenchmarkDisplayEntry, b: BenchmarkDisplayEntry): number => { + const rank = speed_entry_rank(a) - speed_entry_rank(b); + if (rank !== 0) return rank; + // wasm before native within a tier + const kind = (a.name.includes('wasm') ? 0 : 1) - (b.name.includes('wasm') ? 0 : 1); + if (kind !== 0) return kind; + return a.name.localeCompare(b.name); +}; + export const derive_benchmark_groups = (baseline: BenchmarkBaseline): Array => { const grouped: Map> = new Map(); for (const entry of baseline.entries) { @@ -159,29 +279,27 @@ export const derive_benchmark_groups = (baseline: BenchmarkBaseline): Array e.name === canonical_name); - const slowest = Math.max(...entries.map((e) => e.mean_ns)); + // The sort below leads each group with its canonical reference (Prettier for + // format, the JS baseline for parse), so the first row is the default 1.0x + // anchor; the shared component reads that default off the first row and + // recomputes every ratio, re-baselining onto whichever row is hovered. (Size + // groups lead with their smallest build; see `derive_size_groups`.) + // `?? 0` coerces the coverage-only null (unreachable on a perf report, the + // only kind this runs on) so the display entry's `mean_ns` stays a number. + const slowest = Math.max(...entries.map((e) => e.mean_ns ?? 0)); const display_entries: Array = entries.map((e) => ({ name: e.name, - mean_ns: e.mean_ns, - bar_fraction: slowest > 0 ? e.mean_ns / slowest : 0, - speedup_vs_canonical: - canonical_entry_raw && e.name !== canonical_name - ? canonical_entry_raw.mean_ns / e.mean_ns - : undefined, + mean_ns: e.mean_ns ?? 0, + bar_fraction: slowest > 0 ? (e.mean_ns ?? 0) / slowest : 0, category: categorize_name(e.name), files_processed: e.files_processed ?? null, - files_total: e.files_total ?? null, + files_total: e.files_total ?? null })); - // Sort: canonical first, then by mean_ns descending (slowest first for visual) - display_entries.sort((a, b) => { - if (a.category === 'canonical' && b.category !== 'canonical') return -1; - if (b.category === 'canonical' && a.category !== 'canonical') return 1; - return b.mean_ns - a.mean_ns; - }); + // Fixed order (see `compare_speed_entries`): canonical leads as the default 1.0x + // anchor, then biome, oxc, tsv's json wires, then tsv's internal engine + display_entries.sort(compare_speed_entries); const iterated_counts = entries .map((e) => e.files_iterated) @@ -191,27 +309,166 @@ export const derive_benchmark_groups = (baseline: BenchmarkBaseline): Array e.category === 'canonical'), - files_iterated: iterated_counts.length > 0 ? Math.max(...iterated_counts) : null, + files_iterated: iterated_counts.length > 0 ? Math.max(...iterated_counts) : null }); } - // Sort groups: format before parse, then by language - const LANG_ORDER: Record = {svelte: 0, typescript: 1, css: 2}; - const OP_ORDER: Record = {format: 0, parse: 1}; result.sort( (a, b) => - (OP_ORDER[a.operation] ?? 9) - (OP_ORDER[b.operation] ?? 9) || - (LANG_ORDER[a.language] ?? 9) - (LANG_ORDER[b.language] ?? 9), + (OPERATION_ORDER[a.operation] ?? 9) - (OPERATION_ORDER[b.operation] ?? 9) || + (LANGUAGE_ORDER[a.language] ?? 9) - (LANGUAGE_ORDER[b.language] ?? 9) ); + // Neither `biome` nor (for svelte/css) `oxc-parser` has a real entry in every + // parse group. `biome`'s `@biomejs/js-api` never exposes a parser to JS at all + // (only formatting and linting), so no parse group has a real biome entry; + // `oxc-parser` only parses TypeScript/JS, so the svelte and css parse groups + // lack it. Mirror both in as disabled placeholders — biome always, oxc-parser + // only where it's missing — then re-sort so they fall into their fixed slots + // (biome then oxc, right after the canonical row), giving all three parse groups + // one shared entry order. + const ts_parse = result.find((g) => g.operation === 'parse' && g.language === 'typescript'); + const oxc_templates = ts_parse?.entries.filter((e) => e.category === 'oxc') ?? []; + for (const group of result) { + if (group.operation !== 'parse') continue; + const biome_placeholder: BenchmarkDisplayEntry = { + name: 'biome-wasm', + mean_ns: 0, + bar_fraction: 0, + category: 'biome', + files_processed: null, + files_total: null, + disabled: true + }; + const needs_oxc = + group.language !== 'typescript' && !group.entries.some((e) => e.category === 'oxc'); + const oxc_placeholders: Array = needs_oxc + ? oxc_templates.map((e) => ({ + ...e, + bar_fraction: 0, + files_processed: null, + files_total: null, + disabled: true + })) + : []; + group.entries.push(biome_placeholder, ...oxc_placeholders); + group.entries.sort(compare_speed_entries); + } + + // The format-side analogue: `dprint` formats TypeScript/JS only — its + // `@dprint/typescript` plugin rejects CSS and Svelte outright (dprint's CSS and + // HTML plugins are separate Wasm plugins the bench doesn't load) — so the svelte + // and css FORMAT groups have no real dprint entry. Mirror it in as a disabled + // placeholder so all three format groups share one entry order, exactly as + // oxc-parser is mirrored into the svelte/css parse groups above. Guarded on the + // template existing, so a report predating the dprint row renders unchanged. + const ts_format = result.find((g) => g.operation === 'format' && g.language === 'typescript'); + const dprint_templates = ts_format?.entries.filter((e) => e.category === 'dprint') ?? []; + if (dprint_templates.length > 0) { + for (const group of result) { + if (group.operation !== 'format') continue; + if (group.entries.some((e) => e.category === 'dprint')) continue; + group.entries.push( + ...dprint_templates.map((e) => ({ + ...e, + bar_fraction: 0, + files_processed: null, + files_total: null, + disabled: true + })) + ); + group.entries.sort(compare_speed_entries); + } + } + + return result; +}; + +// Parse-conformance coverage (the conformance report's headline metric) + +export interface ConformanceRow { + name: string; + files_processed: number; + files_total: number; + // files_processed / files_total, rendered as the coverage percentage + coverage_fraction: number; +} + +export interface ConformanceGroup { + language: string; + // the language's total discovered files (every row shares it) + files_total: number; + rows: Array; +} + +/** + * One coverage row per ENGINE, not per binding: the conformance headline is + * "which files does this parser accept," which is identical across a tool's + * native/wasm/internal variants — so the `-wasm` and `-internal` duplicates + * are dropped and `tsv-json` stands in for tsv (relabeled plainly, since the + * JSON-materialization qualifier is a speed concern, not a coverage one). + */ +const CONFORMANCE_ENGINE_NAMES: Record = { + 'svelte/compiler': 'svelte/compiler', + 'acorn-typescript': 'acorn-typescript', + 'tsv-json': 'tsv', + 'oxc-parser': 'oxc-parser' +}; + +/** + * Derives per-language parse-coverage groups from a conformance report + * (`corpus_kind: 'conformance'` — parse groups only). Rows are ordered by coverage, + * highest first; entries without coverage data are dropped. + */ +export const derive_conformance_groups = (baseline: BenchmarkBaseline): Array => { + const by_language: Map> = new Map(); + for (const entry of baseline.entries) { + const [operation, language] = entry.group.split('/'); + if (operation !== 'parse' || !language) continue; + const display_name = CONFORMANCE_ENGINE_NAMES[entry.name]; + if (!display_name) continue; + if (entry.files_processed == null || entry.files_total == null) continue; + let rows = by_language.get(language); + if (!rows) { + rows = []; + by_language.set(language, rows); + } + rows.push({ + name: display_name, + files_processed: entry.files_processed, + files_total: entry.files_total, + coverage_fraction: entry.files_total > 0 ? entry.files_processed / entry.files_total : 0 + }); + } + + const result: Array = []; + for (const [language, rows] of by_language) { + // coverage descending (highest acceptance first), name as a stable tiebreak + rows.sort((a, b) => b.coverage_fraction - a.coverage_fraction || a.name.localeCompare(b.name)); + result.push({ + language, + files_total: Math.max(0, ...rows.map((r) => r.files_total)), + rows + }); + } + result.sort((a, b) => (LANGUAGE_ORDER[a.language] ?? 9) - (LANGUAGE_ORDER[b.language] ?? 9)); return result; }; +/** + * Formats a coverage fraction as a percentage with two decimals (`99.85%`), + * FLOORED rather than rounded — rounding would render e.g. 44219/44220 as + * `100.00%` next to a visibly non-total count. Only exact totality reads 100% + * (matching the harness's own `coverage_pct` convention in tsv's report.ts). + */ +export const format_coverage_percent = (fraction: number): string => + `${(Math.floor(fraction * 10_000) / 100).toFixed(2)}%`; + export const derive_speedup_summary = (groups: Array): Array => { const find_speedup = ( operation: string, language: string, - primary_name: string, + primary_name: string ): number | undefined => { const group = groups.find((g) => g.operation === operation && g.language === language); if (!group?.canonical_entry) return undefined; @@ -225,17 +482,277 @@ export const derive_speedup_summary = (groups: Array): Array { + const has_parse = label.includes('parse'); + const has_format = label.includes('format') || label.includes('fmt'); + // a build that does both is a full toolchain (e.g. the combined oxc-parser + oxfmt entry) + if (has_parse && has_format) return 'full'; + if (has_parse) return 'parser'; // tsv parse (ffi), tsv_parse_wasm, oxc-parser + if (has_format) return 'formatter'; // tsv format, oxfmt + return 'full'; // tsv (napi/ffi), tsv_wasm, biome +}; + +export interface SizeDisplayEntry extends BinarySize { + bar_fraction: number; + category: ImplementationCategory; + // a grayed-out, inert placeholder for a build that doesn't exist (e.g. oxfmt's + // absent wasm build) — no bar, no size, just the label held in its slot. Absent + // on real, measured entries. + disabled?: boolean; +} + +export interface SizeCapabilityGroup { + capability: SizeCapability; + heading: string; + // sorted smallest-first, so the leading entry is the default ratio anchor (1.0x) + entries: Array; +} + +const SIZE_CAPABILITY_ORDER: ReadonlyArray<{ + capability: SizeCapability; + heading: string; +}> = [ + { capability: 'full', heading: 'Full toolchain (parse + format)' }, + { capability: 'formatter', heading: 'Formatter' }, + { capability: 'parser', heading: 'Parser' } +]; + +/** Display label for the synthesized combined oxc full-toolchain build. */ +export const OXC_FULL_LABEL = 'oxc-parser + oxfmt (napi)'; + +/** Display label for oxfmt's absent wasm build placeholder — see `derive_size_groups`. */ +export const OXFMT_WASM_LABEL = 'oxfmt (wasm)'; + +/** + * Synthesizes oxc's full-toolchain native build by summing its separately-shipped + * parser (`oxc-parser (napi)`) and formatter (`oxfmt (napi)`) packages — together + * they're the closest equivalent to tsv's single parse+format build, so the entry + * stands beside `tsv (napi)` in the full-toolchain group. Returns `undefined` when + * either half is missing (older baselines), and sums gzip only when both carry it. + */ +const synthesize_oxc_full = (sizes: Array): BinarySize | undefined => { + const parser = sizes.find((s) => s.label === 'oxc-parser (napi)'); + const formatter = sizes.find((s) => s.label === 'oxfmt (napi)'); + if (!parser || !formatter) return undefined; + return { + label: OXC_FULL_LABEL, + bytes: parser.bytes + formatter.bytes, + kind: 'native', + gzip_bytes: + parser.gzip_bytes != null && formatter.gzip_bytes != null + ? parser.gzip_bytes + formatter.gzip_bytes + : null + }; +}; + +/** + * Groups the binary sizes by capability (full / formatter / parser), each group + * mixing wasm and native builds sorted smallest-first. Bars scale to the group's + * largest build; the `vs` ratio anchors on the group's single smallest build, so + * exactly one entry reads 1.0x and every other is a multiple of it. (In the current + * data that smallest build is always one of tsv's, so tsv reads 1.0x and the heavier + * competitors read >1.0x.) A combined `oxc-parser + oxfmt` entry is + * synthesized into the full-toolchain group, since oxc ships parse and format apart. + * oxfmt has no wasm build, so the formatter group gets a disabled `oxfmt (wasm)` + * placeholder slotted just above its real `oxfmt (napi)` entry, holding the slot + * rather than omitting it. + */ +export const derive_size_groups = (sizes: Array): Array => { + const oxc_full = synthesize_oxc_full(sizes); + const all_sizes = oxc_full ? [...sizes, oxc_full] : sizes; + const groups: Array = []; + for (const { capability, heading } of SIZE_CAPABILITY_ORDER) { + const items = all_sizes.filter((s) => categorize_size_capability(s.label) === capability); + if (items.length === 0) continue; + const sorted = items.toSorted((a, b) => a.bytes - b.bytes); + const max = Math.max(0, ...items.map((s) => s.bytes)); + // `sorted` is ascending, so the smallest build leads the group — its single + // default ratio anchor (1.0x), one baseline whether or not the group mixes wasm + // and native (rather than the confusing pair a per-kind anchor produced). The + // shared component reads every ratio from that leading row. + const entries: Array = sorted.map((s) => ({ + ...s, + bar_fraction: max > 0 ? s.bytes / max : 0, + category: categorize_size(s.label) + })); + if (capability === 'formatter' && !entries.some((e) => e.label === OXFMT_WASM_LABEL)) { + const native_index = entries.findIndex((e) => e.label === 'oxfmt (napi)'); + const placeholder: SizeDisplayEntry = { + label: OXFMT_WASM_LABEL, + bytes: 0, + kind: 'wasm', + gzip_bytes: null, + bar_fraction: 0, + category: categorize_size(OXFMT_WASM_LABEL), + disabled: true + }; + entries.splice(native_index === -1 ? entries.length : native_index, 0, placeholder); + } + groups.push({ capability, heading, entries }); + } + return groups; +}; + +// Cross-runtime combined report (the bench composer's `report.json`, `kind: 'combined'`) + +export type BenchmarkRuntime = 'deno' | 'node' | 'bun'; + +export interface CrossRuntimeRow { + group: string; + name: string; + ops_per_second: Partial>; + mean_ns: Partial>; + files_iterated: Partial>; +} + +export interface CrossRuntimeReport { + version: number; + kind: 'combined'; + generated: string; + runtimes: Array; + // The sibling reports came from different commits/versions — ratios are + // unreliable until every runtime is re-run. Present from combined + // `version` 6 on. + mixed_vintage?: boolean; + // The sibling reports were produced on different hardware — ratios are not + // comparable. Present from combined `version` 7 on. + mixed_machine?: boolean; + sources: Array<{ + runtime: BenchmarkRuntime; + timestamp: string; + git_commit: string | null; + tsv: string | null; + // The producing box's machine block; present from combined `version` 7 on. + machine?: Machine | null; + }>; + rows: Array; +} + +export interface CrossRuntimeDisplayRow { + name: string; + category: ImplementationCategory; + ops_per_second: Partial>; + // ratio of each runtime vs the base (first present) runtime; `> 1` = faster + ratio_vs_base: Partial>; +} + +export interface CrossRuntimeGroup { + group: string; + operation: string; + language: string; + rows: Array; +} + +// The combined report stores runtimes deno-first (matching the bench's +// `report.md`); the site presents them node-first (the flagship N-API runtime), +// then deno, then bun. +const CROSS_RUNTIME_DISPLAY_ORDER: Array = ['node', 'deno', 'bun']; + +/** + * The report's runtimes in the site's display order — node (the flagship, the + * ratio anchor) first, then deno, then bun. Shared by `derive_cross_runtime_groups` + * and the table headers so the anchor can't drift between them. + */ +export const order_cross_runtime_runtimes = ( + runtimes: Array +): Array => CROSS_RUNTIME_DISPLAY_ORDER.filter((r) => runtimes.includes(r)); + +/** One runtime's own version string (`node 24.14.1`), for the cross-runtime section. */ +export interface RuntimeVersion { + runtime: BenchmarkRuntime; + version: string; +} + +/** + * The per-runtime version strings from a cross-runtime report, in the site's + * display order (node first). Drops any runtime whose source carries no machine + * block (reports predating combined `version` 7). The cross-runtime section + * renders these so the three-runtime tables disclose which node/deno/bun version + * each column was measured under; the environment panel above stays scoped to the + * flagship Node baseline. The shared hardware identity (CPU/OS/arch) isn't + * repeated here — it lives in the environment panel, and a per-runtime hardware + * mismatch is the report's `mixed_machine` flag's concern. + */ +export const derive_runtime_versions = (report: CrossRuntimeReport): Array => { + const by_runtime = new Map(report.sources.map((source) => [source.runtime, source])); + const result: Array = []; + for (const runtime of order_cross_runtime_runtimes(report.runtimes)) { + const version = by_runtime.get(runtime)?.machine?.runtime_version; + if (version) result.push({ runtime, version }); + } + return result; +}; + +/** + * Groups the combined report's rows by benchmark group, in the same display + * order as `derive_benchmark_groups` (format before parse, then svelte / + * typescript / css). The ratio base is the first runtime in display order + * (node when present — the flagship N-API path), regardless of the report's + * own deno-first storage order. + */ +export const derive_cross_runtime_groups = ( + report: CrossRuntimeReport +): Array => { + const runtimes = order_cross_runtime_runtimes(report.runtimes); + const base = runtimes[0]; + const grouped: Map> = new Map(); + for (const row of report.rows) { + let rows = grouped.get(row.group); + if (!rows) { + rows = []; + grouped.set(row.group, rows); + } + const base_ops = base ? row.ops_per_second[base] : undefined; + const ratio_vs_base: Partial> = {}; + for (const runtime of runtimes) { + const ops = row.ops_per_second[runtime]; + if (ops != null && base_ops != null && base_ops > 0) { + ratio_vs_base[runtime] = ops / base_ops; + } + } + rows.push({ + name: row.name, + category: categorize_name(row.name), + ops_per_second: row.ops_per_second, + ratio_vs_base + }); + } + + const result: Array = []; + for (const [group, rows] of grouped) { + const [operation, language] = group.split('/'); + result.push({ group, operation: operation!, language: language!, rows }); + } + result.sort( + (a, b) => + (OPERATION_ORDER[a.operation] ?? 9) - (OPERATION_ORDER[b.operation] ?? 9) || + (LANGUAGE_ORDER[a.language] ?? 9) - (LANGUAGE_ORDER[b.language] ?? 9) + ); + return result; +}; + // Formatting utilities export interface FormattedUnit { @@ -244,32 +761,109 @@ export interface FormattedUnit { } export const format_ns = (ns: number): FormattedUnit => { - if (ns < 1_000) return {value: `${Math.round(ns)}`, unit: 'ns'}; + if (ns < 1_000) return { value: `${Math.round(ns)}`, unit: 'ns' }; if (ns < 1_000_000) return { value: (ns / 1_000).toFixed(ns < 10_000 ? 2 : ns < 100_000 ? 1 : 0), - unit: 'µs', + unit: 'µs' }; const ms = Math.round(ns / 1_000_000); - return {value: ms.toLocaleString('en-US'), unit: 'ms'}; + return { value: ms.toLocaleString('en-US'), unit: 'ms' }; }; export const format_bytes = (bytes: number): FormattedUnit => { - if (bytes < 1_024) return {value: `${bytes}`, unit: 'B'}; - if (bytes < 1_048_576) return {value: (bytes / 1_024).toFixed(0), unit: 'KB'}; - return {value: (bytes / 1_048_576).toFixed(1), unit: 'MB'}; + if (bytes < 1_024) return { value: `${bytes}`, unit: 'B' }; + if (bytes < 1_048_576) return { value: (bytes / 1_024).toFixed(0), unit: 'KB' }; + return { value: (bytes / 1_048_576).toFixed(1), unit: 'MB' }; +}; + +/** + * Formats a corpus source's file count as a per-language breakdown + * (`124 typescript, 15 svelte, 31 css`), largest language first and dropping + * zero-count languages. Falls back to the plain `N files` total when the report + * predates the per-language split (or lists no recognized language). + */ +export const format_corpus_source_files = (source: CorpusSource): string => { + const total = `${source.files.toLocaleString('en-US')} files`; + if (!source.by_language) return total; + const parts = Object.entries(source.by_language) + .filter((entry): entry is [string, number] => (entry[1] ?? 0) > 0) + .sort((a, b) => b[1] - a[1]) + .map(([language, count]) => `${count.toLocaleString('en-US')} ${language}`); + return parts.length > 0 ? parts.join(', ') : total; }; +// Corpus source repos (site-owned path → URL mapping) + +export interface CorpusRepo { + // public repo URL the entry links to + url: string; + // `org/name`, derived from the URL — the linkified display label + label: string; +} + +/** + * Maps a corpus source's on-disk path to its public repo URL. The bench records + * only local paths (`../zzz/src`), so the site owns this mapping — the links then + * survive an `update-benchmarks` refresh without touching the copied JSON. Keyed by + * the repo's directory prefix, each ending in a slash so siblings like `../fuz_css/` + * and `../fuz_code/`, and `../svelte/` vs `../svelte.dev/`, stay distinct. A source + * matching no prefix — the derived `svelte_styles` CSS cache, which isn't a single + * repo — is dropped from the repos list rather than shown unlinked. + */ +const CORPUS_REPO_URL_BY_PREFIX: ReadonlyArray = [ + ['../zzz/', 'https://github.com/fuzdev/zzz'], + ['../fuz_app/', 'https://github.com/fuzdev/fuz_app'], + ['../fuz_blog/', 'https://github.com/fuzdev/fuz_blog'], + ['../fuz_code/', 'https://github.com/fuzdev/fuz_code'], + ['../fuz_css/', 'https://github.com/fuzdev/fuz_css'], + ['../fuz_docs/', 'https://github.com/fuzdev/fuz_docs'], + ['../fuz_gitops/', 'https://github.com/fuzdev/fuz_gitops'], + ['../fuz_mastodon/', 'https://github.com/fuzdev/fuz_mastodon'], + ['../fuz_template/', 'https://github.com/fuzdev/fuz_template'], + ['../fuz_ui/', 'https://github.com/fuzdev/fuz_ui'], + ['../fuz_util/', 'https://github.com/fuzdev/fuz_util'], + ['../mdz/', 'https://github.com/fuzdev/mdz'], + ['../gro/', 'https://github.com/fuzdev/gro'], + ['../svelte-docinfo/', 'https://github.com/fuzdev/svelte-docinfo'], + ['../tsv.fuz.dev/', 'https://github.com/fuzdev/tsv.fuz.dev'], + ['../ryanatkn.com/', 'https://github.com/ryanatkn/ryanatkn.com'], + ['../webdevladder.net/', 'https://github.com/ryanatkn/webdevladder.net'], + ['../kit/', 'https://github.com/sveltejs/kit'], + ['../svelte.dev/', 'https://github.com/sveltejs/svelte.dev'], + ['../svelte/', 'https://github.com/sveltejs/svelte'] +]; + +/** The repo URL for a corpus source path, or `undefined` when it maps to no repo. */ +const corpus_repo_url = (path: string): string | undefined => + CORPUS_REPO_URL_BY_PREFIX.find(([prefix]) => path.startsWith(prefix))?.[1]; + +/** `org/name` from a `https://github.com/org/name` URL — the linkified label. */ +const corpus_repo_label = (url: string): string => new URL(url).pathname.slice(1); + /** - * Formats per-implementation corpus coverage as `processed/total`, or - * `undefined` when either value is missing (older baselines without coverage). + * The distinct source repos behind a report's corpus, one entry per URL in + * first-seen order (the author's ecosystem leads, the upstream framework repos + * trail, matching the source order). Sources sharing a repo (svelte.dev's several + * packages) collapse to one entry; sources with no mapped repo (the `svelte_styles` + * CSS cache) are dropped. URLs come from the site-owned `CORPUS_REPO_URL_BY_PREFIX`, + * not the report, so the links survive a refresh. */ -export const format_coverage = ( - processed: number | null | undefined, - total: number | null | undefined, -): string | undefined => { - if (processed == null || total == null) return undefined; - return `${processed}/${total}`; +export const derive_corpus_repos = ( + sources: Array | undefined +): Array => { + const by_url: Map = new Map(); + for (const source of sources ?? []) { + // Prefer the report's git-detected repo; fall back to the legacy prefix + // map for reports predating `version` 8 (which lack `source.repo`). + const url = source.repo?.url ?? corpus_repo_url(source.path); + if (!url || by_url.has(url)) continue; + by_url.set(url, { + url, + label: source.repo?.slug ?? corpus_repo_label(url) + }); + } + return [...by_url.values()]; }; /** @@ -279,7 +873,7 @@ export const format_coverage = ( */ export const format_gzip_size = (gzip_bytes: number | null | undefined): string | undefined => { if (gzip_bytes == null) return undefined; - const {value, unit} = format_bytes(gzip_bytes); + const { value, unit } = format_bytes(gzip_bytes); return `${value} ${unit} gz`; }; @@ -287,10 +881,92 @@ export const format_gzip_size = (gzip_bytes: number | null | undefined): string export const format_speedup = (ratio: number): string => ratio >= 10 ? `${ratio.toFixed(1)}x` : `${ratio.toFixed(2)}x`; +/** + * Loose ratio formatting for prose, which reads better with fewer digits than a + * table column (`1.7x`, `26x`) — always paired with a `~` in the copy. Renders + * `—` for a missing ratio rather than throwing mid-sentence; the benchmark tests + * assert every ratio the page quotes actually resolves. + */ +export const format_ratio_approx = (ratio: number | undefined): string => + ratio === undefined ? '—' : ratio >= 10 ? `${Math.round(ratio)}x` : `${ratio.toFixed(1)}x`; + +/** + * An inclusive ratio range for prose (`3–9x`), FLOORED at both ends so a + * "3–9x less memory" claim never overstates either bound. + */ +export const format_ratio_range = (min: number, max: number): string => + `${Math.floor(min)}–${Math.floor(max)}x`; + +/** + * How many times faster `faster` is than `slower` within one `operation/language` + * group, by mean time — the ratio the prose summaries quote. + * + * @returns the ratio, or `undefined` when either entry is absent from the report + */ +export const benchmark_speedup = ( + baseline: BenchmarkBaseline, + group: string, + slower: string, + faster: string +): number | undefined => { + const find = (name: string) => baseline.entries.find((e) => e.group === group && e.name === name); + // a coverage-only report carries null timings, so both sides must be real + const a = find(slower)?.mean_ns; + const b = find(faster)?.mean_ns; + if (a == null || !b) return undefined; + return a / b; +}; + +/** + * Signed speedup: entries at or above the anchor read as a plain multiple + * (`2.50x`), while slower entries show the reciprocal negated (`0.15x` → `-6.67x`) + * so "how many times slower" is directly legible instead of a fraction the reader + * has to invert. The minus is a convention for "times slower", not a literal + * negative rate. Used for the format/parse speed bars, where entries span widely + * on both sides of the anchor; the near-parity cross-runtime table and the + * bigger-is-worse size ratios stay on the plain fractional `format_speedup`. + */ +export const format_speedup_signed = (ratio: number): string => { + const magnitude = ratio >= 1 ? ratio : 1 / ratio; + const digits = magnitude >= 10 ? 1 : 2; + return `${ratio < 1 ? '-' : ''}${magnitude.toFixed(digits)}x`; +}; + /** Hyphenated tool names that should preserve their hyphens in display labels. */ const HYPHENATED_NAMES = ['acorn-typescript', 'oxc-parser']; +/** + * Display labels for the raw benchmark entry names in the main (Node) tables, + * annotating each with the runtime **and** binding it runs under — the native + * builds load the N-API addon under Node, so they read `(node napi)` to + * distinguish them from the Deno FFI numbers the cross-runtime table surfaces + * (`tsv (deno ffi)`); the third-party wasm builds are marked `(wasm)`. Mirrors + * the parenthesized suffixes the binary-size section's labels already carry. + * tsv's own wasm entries keep their `tsv_wasm` package-name style, as in the size + * groups, so they aren't listed here. The already-parenthesized size labels + * aren't keys, so they fall through to the generic formatting below unchanged. + * The cross-runtime table neutralizes the `(node napi)` suffix per row (its + * columns span runtimes) via `format_cross_runtime_label`. + */ +const LABEL_OVERRIDES: Record = { + tsv: 'tsv (node napi)', + 'tsv-json': 'tsv json (node napi)', + // `no-locs` (not `no-locations`) — the full word eats too much column width. + 'tsv-json-no-locations': 'tsv json no-locs (node napi)', + // the one tsv_wasm entry listed here: the generic formatting below would + // break the `no-locs` hyphen its native sibling deliberately keeps + 'tsv_wasm-json-no-locations': 'tsv_wasm json no-locs', + 'tsv-internal': 'tsv internal (node napi)', + 'oxc-parser': 'oxc-parser (node napi)', + oxfmt: 'oxfmt (node napi)', + 'biome-wasm': 'biome (wasm)', + 'dprint-wasm': 'dprint (wasm)', + 'oxc-parser-wasm': 'oxc-parser (wasm)' +}; + export const format_label = (name: string): string => { + const override = LABEL_OVERRIDES[name]; + if (override) return override; for (const tool of HYPHENATED_NAMES) { if (name.startsWith(tool)) { return tool + name.slice(tool.length).replaceAll('-', ' '); @@ -299,6 +975,16 @@ export const format_label = (name: string): string => { return name.replaceAll('-', ' '); }; +/** + * The label for a cross-runtime row. Each row spans node/deno/bun columns, and a + * native build's binding is runtime-specific (N-API on node & bun, C-FFI on + * deno), so the row can't pin one binding — neutralize `format_label`'s + * node-centric `(node napi)` suffix to `(native)`. The per-runtime binding is + * disclosed once in the table caption instead (see `BenchmarksCrossRuntime`). + */ +export const format_cross_runtime_label = (name: string): string => + format_label(name).replace(' (node napi)', ' (native)'); + /** Returns a CSS background color variable for a category. */ export const category_color = (category: ImplementationCategory): string => { switch (category) { @@ -314,16 +1000,24 @@ export const category_color = (category: ImplementationCategory): string => { return 'var(--color_f_40)'; case 'biome': return 'var(--color_a_40)'; + case 'dprint': + return 'var(--color_b_40)'; case 'oxc': return 'var(--color_i_40)'; } }; -/** Returns a CSS color variable for a size ratio (inverted — bigger is worse). */ +/** + * Returns a CSS color variable for a size ratio (inverted — bigger is worse). + * Green is reserved for builds smaller than the baseline (ratio < 1, only reachable + * once a hover re-baselines onto a larger build); any build at or above the baseline + * reads yellow at the floor, ramping through orange to red as it grows. + */ export const size_ratio_color = (ratio: number): string => { - if (ratio < 2) return 'var(--color_b_50)'; // green — similar - if (ratio < 5) return 'var(--color_e_50)'; // yellow — notably larger - return 'var(--color_c_50)'; // red — much larger + if (ratio < 1) return 'var(--color_b_50)'; // green — smaller than baseline + if (ratio < 3) return 'var(--color_e_50)'; // yellow — larger + if (ratio < 10) return 'var(--color_h_50)'; // orange — much larger + return 'var(--color_c_50)'; // red — enormous }; /** Returns a CSS color variable for the speedup ratio. */ @@ -334,3 +1028,73 @@ export const speedup_color = (ratio: number): string => { if (ratio < 5) return 'var(--color_b_50)'; // green — fast return 'var(--color_j_50)'; // teal — exceptional }; + +/** + * Cross-runtime ratio cell background: a stable fuz_css red (`color_c`) / green + * (`color_b`) whose ALPHA varies with distance from parity. Fully transparent at ratio + * `1.0` (parity recedes), ramping to `0.3` alpha at ratio `0.8` and below (red) or `1.2` + * and above (green). The hue stays constant — only opacity moves — so it reads + * consistently in light and dark themes while the cell's text keeps the default color. + * Deliberately its own scale — NOT the shared `speedup_color` — because cross-runtime + * deltas cluster tightly near 1.0 and this must not bleed into the other displays. + */ +export const cross_runtime_ratio_background = (ratio: number): string => { + // 0 alpha at ratio 1.0, up to 0.3 at ratio ≤ 0.8 (red) or ≥ 1.2 (green) + const alpha = Math.min(1, Math.abs(ratio - 1) / 0.2) * 0.3; + const color = ratio < 1 ? 'var(--color_c_50)' : 'var(--color_b_50)'; + return `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(1)}%, transparent)`; +}; + +// Interactive baseline (hover-to-rebaseline) + +/** + * Which way a group's ratio runs, so re-anchoring on hover stays consistent. A + * `speed` group (format/parse) reads its anchor as a reference speed — faster + * entries are positive multiples, slower ones negative (`format_speedup_signed` / + * `speedup_color`). A `size` group reads its anchor as a reference size — bigger + * builds are multiples ≥ 1 (`format_size_ratio` / `size_ratio_color`). The two + * ratios are reciprocals, which is exactly why one direction flag suffices. + */ +export type BaselineDirection = 'speed' | 'size'; + +/** An entry's ratio against its group's anchor, in the group's direction. */ +export const compute_baseline_ratio = ( + direction: BaselineDirection, + entry_raw: number, + anchor_raw: number +): number => (direction === 'speed' ? anchor_raw / entry_raw : entry_raw / anchor_raw); + +/** Plain size ratio (`2.3x`) — its own formatter since sizes never take the signed treatment. */ +export const format_size_ratio = (ratio: number): string => `${ratio.toFixed(1)}x`; + +/** Formats a baseline ratio for display in the given direction. */ +export const format_baseline_ratio = (direction: BaselineDirection, ratio: number): string => + direction === 'speed' ? format_speedup_signed(ratio) : format_size_ratio(ratio); + +/** Color for a baseline ratio in the given direction. */ +export const baseline_ratio_color = (direction: BaselineDirection, ratio: number): string => + direction === 'speed' ? speedup_color(ratio) : size_ratio_color(ratio); + +/** + * A normalized row for `BenchmarksBaselineGroup` — the shared hover-to-rebaseline + * column behind the format, parse, and binary-size groups. `raw` is the number the + * ratio derives from (sweep mean ns for speed, bytes for size); `value` is the + * row's displayed measurement (the whole-sweep mean — total time over the group's + * iterated corpus — for speed, bytes for size). Flattening the group-specific + * display entries to this one shape lets a single component own the anchor state + * for all three sections. + */ +export interface BaselineRow { + // stable identity for `#each` and anchor matching (the entry name / size label) + key: string; + // raw name/label; `BenchmarksBar` formats it for display + label: string; + category: ImplementationCategory; + bar_fraction: number; + value: FormattedUnit; + // the number the ratio derives from — mean ns (speed) or bytes (size) + raw: number; + annotation: string | undefined; + // grayed-out, inert placeholder — never an anchor, no hover highlight + disabled: boolean; +} diff --git a/src/routes/docs/benchmarks/benchmarks.json b/src/routes/docs/benchmarks/benchmarks.json index bf56417..171cc7a 100644 --- a/src/routes/docs/benchmarks/benchmarks.json +++ b/src/routes/docs/benchmarks/benchmarks.json @@ -1,685 +1,1189 @@ { - "version": 4, - "timestamp": "2026-06-12T09:22:00.588Z", - "git_commit": "98918f9", + "version": 7, + "runtime": "node", + "corpus_kind": "perf", + "timestamp": "2026-07-22T09:18:34.995Z", + "git_commit": "d063479a", + "machine": { + "cpu_model": "AMD Ryzen 5 PRO 7530U with Radeon Graphics", + "os": "linux", + "arch": "x86_64", + "runtime_version": "24.14.1" + }, "corpus": { - "svelte": 1240, - "typescript": 4025, - "css": 193 + "svelte": 767, + "typescript": 2448, + "css": 50 }, + "corpus_sources": [ + { + "path": "../zzz/src", + "files": 326, + "by_language": { + "svelte": 173, + "typescript": 152, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/zzz", + "slug": "fuzdev/zzz", + "commit": "838d499c7cc746ebe1f5da8470e7679c8895aee0", + "subpath": "src" + } + }, + { + "path": "../fuz_app/src", + "files": 665, + "by_language": { + "svelte": 33, + "typescript": 631, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_app", + "slug": "fuzdev/fuz_app", + "commit": "4f012eae9ff11e5a1fb7a4faacf640ea9b63607a", + "subpath": "src" + } + }, + { + "path": "../fuz_blog/src", + "files": 37, + "by_language": { + "svelte": 20, + "typescript": 16, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_blog", + "slug": "fuzdev/fuz_blog", + "commit": "ecc86884c6e1ebe31d1f492e5df631cb8e586273", + "subpath": "src" + } + }, + { + "path": "../fuz_code/src", + "files": 74, + "by_language": { + "svelte": 20, + "typescript": 50, + "css": 4 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_code", + "slug": "fuzdev/fuz_code", + "commit": "fea306ae68aa9f40155e108242d0b7168b49c41f", + "subpath": "src" + } + }, + { + "path": "../fuz_css/src", + "files": 146, + "by_language": { + "svelte": 39, + "typescript": 105, + "css": 2 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_css", + "slug": "fuzdev/fuz_css", + "commit": "9263f076e8b23c64564a2236b886279d1b42d626", + "subpath": "src" + } + }, + { + "path": "../fuz_docs/src", + "files": 65, + "by_language": { + "svelte": 45, + "typescript": 19, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_docs", + "slug": "fuzdev/fuz_docs", + "commit": "e27d7b7acbff67a442354cbd318df5d2c1f78552", + "subpath": "src" + } + }, + { + "path": "../fuz_gitops/src", + "files": 99, + "by_language": { + "svelte": 28, + "typescript": 70, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_gitops", + "slug": "fuzdev/fuz_gitops", + "commit": "6d1a90269e16559259d7660ba1b7b119da5967ac", + "subpath": "src" + } + }, + { + "path": "../fuz_mastodon/src", + "files": 25, + "by_language": { + "svelte": 15, + "typescript": 9, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_mastodon", + "slug": "fuzdev/fuz_mastodon", + "commit": "6179d626e50dd1a53540a074fc23c354c2985321", + "subpath": "src" + } + }, + { + "path": "../fuz_template/src", + "files": 18, + "by_language": { + "svelte": 10, + "typescript": 7, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_template", + "slug": "fuzdev/fuz_template", + "commit": "664498efdd7952010a314dd5e93f5ca0b269881e", + "subpath": "src" + } + }, + { + "path": "../fuz_ui/src", + "files": 216, + "by_language": { + "svelte": 121, + "typescript": 95, + "css": 0 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_ui", + "slug": "fuzdev/fuz_ui", + "commit": "067e491c40b360e150e24b2c591942f505d0046f", + "subpath": "src" + } + }, + { + "path": "../fuz_util/src", + "files": 145, + "by_language": { + "svelte": 8, + "typescript": 136, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/fuz_util", + "slug": "fuzdev/fuz_util", + "commit": "f7887bc67ba8398d4e1f858b319f2d83e7455d3e", + "subpath": "src" + } + }, + { + "path": "../mdz/src", + "files": 71, + "by_language": { + "svelte": 22, + "typescript": 48, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/mdz", + "slug": "fuzdev/mdz", + "commit": "860b62134eb9ecafd7a52240414e87bb481448f4", + "subpath": "src" + } + }, + { + "path": "../gro/src", + "files": 156, + "by_language": { + "svelte": 10, + "typescript": 146, + "css": 0 + }, + "repo": { + "url": "https://github.com/fuzdev/gro", + "slug": "fuzdev/gro", + "commit": "32af831867a0e73f98b4a6f2d35f435e21fea7db", + "subpath": "src" + } + }, + { + "path": "../svelte-docinfo/src", + "files": 99, + "by_language": { + "svelte": 22, + "typescript": 76, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/svelte-docinfo", + "slug": "fuzdev/svelte-docinfo", + "commit": "39e10bdff2ca77a82a8c017bbff004cc2f809aa0", + "subpath": "src" + } + }, + { + "path": "../tsv.fuz.dev/src", + "files": 34, + "by_language": { + "svelte": 17, + "typescript": 16, + "css": 1 + }, + "repo": { + "url": "https://github.com/fuzdev/tsv.fuz.dev", + "slug": "fuzdev/tsv.fuz.dev", + "commit": "a370a32a1dbe33c55026708019d0e403a91b13c3", + "subpath": "src" + } + }, + { + "path": "../ryanatkn.com/src", + "files": 52, + "by_language": { + "svelte": 31, + "typescript": 20, + "css": 1 + }, + "repo": { + "url": "https://github.com/ryanatkn/ryanatkn.com", + "slug": "ryanatkn/ryanatkn.com", + "commit": "590ca7be62869763c0aa5fe7531ab9b4b00b275e", + "subpath": "src" + } + }, + { + "path": "../webdevladder.net/src", + "files": 39, + "by_language": { + "svelte": 24, + "typescript": 14, + "css": 1 + }, + "repo": { + "url": "https://github.com/ryanatkn/webdevladder.net", + "slug": "ryanatkn/webdevladder.net", + "commit": "9262030ba9469383ea83f88978f18779d0712566", + "subpath": "src" + } + }, + { + "path": "benches/js/.cache/svelte_styles", + "files": 18, + "by_language": { + "svelte": 0, + "typescript": 0, + "css": 18 + } + }, + { + "path": "../kit/packages/kit/src", + "files": 297, + "by_language": { + "svelte": 26, + "typescript": 271, + "css": 0 + }, + "repo": { + "url": "https://github.com/sveltejs/kit", + "slug": "sveltejs/kit", + "commit": "da5b08ea7f5182f505b5d1cb9b347effbcb35231", + "subpath": "packages/kit/src" + } + }, + { + "path": "../svelte/packages/svelte/src", + "files": 415, + "by_language": { + "svelte": 0, + "typescript": 415, + "css": 0 + }, + "repo": { + "url": "https://github.com/sveltejs/svelte", + "slug": "sveltejs/svelte", + "commit": "b4d1583ae20f3869a88a731d9a265c546c099f66", + "subpath": "packages/svelte/src" + } + }, + { + "path": "../svelte.dev/apps/svelte.dev/src", + "files": 145, + "by_language": { + "svelte": 60, + "typescript": 85, + "css": 0 + }, + "repo": { + "url": "https://github.com/sveltejs/svelte.dev", + "slug": "sveltejs/svelte.dev", + "commit": "c21c2d0f011915b2a783c612c978a348cec98a5c", + "subpath": "apps/svelte.dev/src" + } + }, + { + "path": "../svelte.dev/packages/repl/src", + "files": 53, + "by_language": { + "svelte": 17, + "typescript": 34, + "css": 2 + }, + "repo": { + "url": "https://github.com/sveltejs/svelte.dev", + "slug": "sveltejs/svelte.dev", + "commit": "c21c2d0f011915b2a783c612c978a348cec98a5c", + "subpath": "packages/repl/src" + } + }, + { + "path": "../svelte.dev/packages/site-kit/src", + "files": 70, + "by_language": { + "svelte": 26, + "typescript": 33, + "css": 11 + }, + "repo": { + "url": "https://github.com/sveltejs/svelte.dev", + "slug": "sveltejs/svelte.dev", + "commit": "c21c2d0f011915b2a783c612c978a348cec98a5c", + "subpath": "packages/site-kit/src" + } + } + ], "versions": { - "tsv": "0.1.0", - "svelte": "5.56.1", + "tsv": "0.2.0", + "svelte": "5.56.4", "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" + "acorn_ts": "1.0.11", + "prettier": "3.9.5", + "prettier_svelte": "4.1.1", + "oxc_parser": "0.140.0", + "oxfmt": "0.60.0", + "biome": "2.5.4", + "dprint": "0.96.1" }, "binary_sizes": [ { - "label": "tsv (native)", - "bytes": 3861760, + "label": "tsv (ffi)", + "bytes": 3346544, + "kind": "native", + "gzip_bytes": 1442507 + }, + { + "label": "tsv format (ffi)", + "bytes": 3095968, "kind": "native", - "gzip_bytes": 1622771 + "gzip_bytes": 1339168 + }, + { + "label": "tsv parse (ffi)", + "bytes": 1606912, + "kind": "native", + "gzip_bytes": 703248 + }, + { + "label": "tsv (napi)", + "bytes": 3479392, + "kind": "native", + "gzip_bytes": 1486322 }, { "label": "tsv_format_wasm", - "bytes": 2196292, + "bytes": 2216330, "kind": "wasm", - "gzip_bytes": 715422 + "gzip_bytes": 787915 }, { "label": "tsv_parse_wasm", - "bytes": 1699625, + "bytes": 1052601, "kind": "wasm", - "gzip_bytes": 526578 + "gzip_bytes": 388661 }, { "label": "tsv_wasm", - "bytes": 2888918, + "bytes": 2444292, "kind": "wasm", - "gzip_bytes": 911323 + "gzip_bytes": 867803 }, { "label": "biome (wasm)", - "bytes": 34430368, + "bytes": 38584187, "kind": "wasm", - "gzip_bytes": 8187702 + "gzip_bytes": 9281487 }, { - "label": "oxc-parser (native)", - "bytes": 2658008, + "label": "dprint (wasm)", + "bytes": 4174185, + "kind": "wasm", + "gzip_bytes": 1155127 + }, + { + "label": "oxc-parser (napi)", + "bytes": 2354904, "kind": "native", - "gzip_bytes": 1032774 + "gzip_bytes": 954754 }, { - "label": "oxfmt (native)", - "bytes": 8016464, + "label": "oxfmt (napi)", + "bytes": 8844056, "kind": "native", - "gzip_bytes": 3224684 + "gzip_bytes": 3575335 }, { "label": "oxc-parser (wasm)", - "bytes": 1866798, + "bytes": 1531455, "kind": "wasm", - "gzip_bytes": 518682 + "gzip_bytes": 495227 } ], "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 + "mean_ns": 450117289.7, + "p50_ns": 449990982, + "p75_ns": 454086904, + "p90_ns": 460782361.3, + "p95_ns": 464255661.1, + "p99_ns": 467309973.02, + "min_ns": 446842297, + "max_ns": 468073551, + "std_dev_ns": 3145025.5647039055, + "cv": 0.006987124548803808, + "ops_per_second": 2.2216431647548864, + "sample_size": 10, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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 + "mean_ns": 215054374.7368421, + "p50_ns": 215108675, + "p75_ns": 216642780.75, + "p90_ns": 221466791.9, + "p95_ns": 222243263.35, + "p99_ns": 222392394.47, + "min_ns": 214038777, + "max_ns": 222411234, + "std_dev_ns": 885187.5854249354, + "cv": 0.00411611057207333, + "ops_per_second": 4.6499867822902035, + "sample_size": 19, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" + }, + { + "name": "tsv-json-no-locations", + "group": "parse/svelte", + "mean_ns": 136940898.48387095, + "p50_ns": 137100604, + "p75_ns": 138022550, + "p90_ns": 141696235, + "p95_ns": 142315365, + "p99_ns": 142784485, + "min_ns": 134880981, + "max_ns": 142856476, + "std_dev_ns": 1406524.9125825886, + "cv": 0.010271036105026363, + "ops_per_second": 7.302420321988621, + "sample_size": 31, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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 + "mean_ns": 239961216.70588234, + "p50_ns": 239843331, + "p75_ns": 242042203, + "p90_ns": 244289741, + "p95_ns": 245784287, + "p99_ns": 245964537.4, + "min_ns": 238927466, + "max_ns": 246009600, + "std_dev_ns": 1100810.371221286, + "cv": 0.0045874511987099, + "ops_per_second": 4.16734009656939, + "sample_size": 17, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" + }, + { + "name": "tsv_wasm-json-no-locations", + "group": "parse/svelte", + "mean_ns": 155138759.1153846, + "p50_ns": 155155418, + "p75_ns": 156264183, + "p90_ns": 159277904.4, + "p95_ns": 159735132.8, + "p99_ns": 160006222.28, + "min_ns": 153705591, + "max_ns": 160051685, + "std_dev_ns": 786708.3806164177, + "cv": 0.005070998279877323, + "ops_per_second": 6.445842455503006, + "sample_size": 26, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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 + "mean_ns": 20330465.730088495, + "p50_ns": 20197596, + "p75_ns": 20708728, + "p90_ns": 20865536.5, + "p95_ns": 20965844.5, + "p99_ns": 21051817.8, + "min_ns": 19995227, + "max_ns": 21250059, + "std_dev_ns": 287750.5964940437, + "cv": 0.014153664766674835, + "ops_per_second": 49.18726473245663, + "sample_size": 226, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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 + "mean_ns": 27634829.2109375, + "p50_ns": 27607981.5, + "p75_ns": 28245700, + "p90_ns": 28455070.2, + "p95_ns": 28526437.65, + "p99_ns": 28606624.3, + "min_ns": 27483201, + "max_ns": 28758034, + "std_dev_ns": 181690.4859527825, + "cv": 0.006574691834204346, + "ops_per_second": 36.1862196566141, + "sample_size": 128, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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 + "mean_ns": 4391740370.714286, + "p50_ns": 4380682349, + "p75_ns": 4412690367, + "p90_ns": 4435167488, + "p95_ns": 4441739174, + "p99_ns": 4446996522.8, + "min_ns": 4355169372, + "max_ns": 4448310860, + "std_dev_ns": 35134200.08841751, + "cv": 0.008000063100884804, + "ops_per_second": 0.22770016339498617, + "sample_size": 7, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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 + "mean_ns": 68011448.20289855, + "p50_ns": 67663689.5, + "p75_ns": 69131073.5, + "p90_ns": 69616594.5, + "p95_ns": 69748928.95, + "p99_ns": 71027832.83999999, + "min_ns": 67189585, + "max_ns": 73851102, + "std_dev_ns": 843409.4539816556, + "cv": 0.012400992425062209, + "ops_per_second": 14.703406947264233, + "sample_size": 69, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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 + "mean_ns": 97308671.10416667, + "p50_ns": 96982593, + "p75_ns": 98709308.25, + "p90_ns": 99791989.3, + "p95_ns": 100754954.55, + "p99_ns": 100989705.23, + "min_ns": 96112674, + "max_ns": 101040464, + "std_dev_ns": 1164641.1530361492, + "cv": 0.011968523871725962, + "ops_per_second": 10.276576472095927, + "sample_size": 48, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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 + "mean_ns": 4328676301.428572, + "p50_ns": 4312345072, + "p75_ns": 4375547937, + "p90_ns": 4393736093.4, + "p95_ns": 4399416073.2, + "p99_ns": 4403960057.04, + "min_ns": 4256952931, + "max_ns": 4405096053, + "std_dev_ns": 56768693.307661824, + "cv": 0.013114561901735811, + "ops_per_second": 0.23101750520591594, + "sample_size": 7, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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 + "mean_ns": 908653574.3333334, + "p50_ns": 907743476, + "p75_ns": 917705754.75, + "p90_ns": 923123625, + "p95_ns": 925151147.5, + "p99_ns": 926773165.5, + "min_ns": 892632974, + "max_ns": 927178670, + "std_dev_ns": 13432537.999488356, + "cv": 0.014782903384651982, + "ops_per_second": 1.1005294297485004, + "sample_size": 6, + "files_processed": 767, + "files_total": 767, + "files_iterated": 767, + "runtime": "node" }, { "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, + "mean_ns": 3128887486.2, + "p50_ns": 3128799385, + "p75_ns": 3132110168, + "p90_ns": 3132563774, + "p95_ns": 3132714976, + "p99_ns": 3132835937.6, + "min_ns": 3125296462, + "max_ns": 3132866178, + "std_dev_ns": 3589049.854497316, + "cv": 0.0011470690046627974, + "ops_per_second": 0.319602416005853, "sample_size": 5, - "files_processed": 3693, - "files_total": 4025, - "files_iterated": 3603 + "files_processed": 2445, + "files_total": 2448, + "files_iterated": 2445, + "runtime": "node" }, { "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, + "mean_ns": 2036068900, + "p50_ns": 2036373272, + "p75_ns": 2037932744, + "p90_ns": 2042044840.4, + "p95_ns": 2043415539.2, + "p99_ns": 2044512098.24, + "min_ns": 2034690477, + "max_ns": 2044786238, + "std_dev_ns": 1424825.1027755418, + "cv": 0.0006997921842308685, + "ops_per_second": 0.49114251487265487, + "sample_size": 4, + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2445, + "runtime": "node" + }, + { + "name": "tsv-json-no-locations", + "group": "parse/typescript", + "mean_ns": 1006124754.2, + "p50_ns": 1005276990, + "p75_ns": 1007632481, + "p90_ns": 1007854793, + "p95_ns": 1007928897, + "p99_ns": 1007988180.2, + "min_ns": 1004502634, + "max_ns": 1008003001, + "std_dev_ns": 1580367.7301149566, + "cv": 0.0015707472890591529, + "ops_per_second": 0.9939125300571001, "sample_size": 5, - "files_processed": 3743, - "files_total": 4025, - "files_iterated": 3603 + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2445, + "runtime": "node" }, { "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, + "mean_ns": 2160965352.2, + "p50_ns": 2161367082, + "p75_ns": 2164544152, + "p90_ns": 2169681129.4, + "p95_ns": 2171393455.2, + "p99_ns": 2172763315.84, + "min_ns": 2150546632, + "max_ns": 2173105781, + "std_dev_ns": 8680789.960942276, + "cv": 0.004017088914500495, + "ops_per_second": 0.46275614691458916, "sample_size": 5, - "files_processed": 3743, - "files_total": 4025, - "files_iterated": 3603 + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2445, + "runtime": "node" + }, + { + "name": "tsv_wasm-json-no-locations", + "group": "parse/typescript", + "mean_ns": 1091721513.6666667, + "p50_ns": 1091788165, + "p75_ns": 1093452294, + "p90_ns": 1093973845.8, + "p95_ns": 1094147696.4, + "p99_ns": 1094286776.88, + "min_ns": 1091656195, + "max_ns": 1094321547, + "std_dev_ns": 65995.0924337055, + "cv": 0.0000604504826620607, + "ops_per_second": 0.9159845138907174, + "sample_size": 3, + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2445, + "runtime": "node" }, { "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 + "mean_ns": 140078986.69444445, + "p50_ns": 139384058, + "p75_ns": 140847581.25, + "p90_ns": 142696759.5, + "p95_ns": 142841338, + "p99_ns": 142886196.25, + "min_ns": 138381808, + "max_ns": 142906624, + "std_dev_ns": 1517145.3312055282, + "cv": 0.01083064181863973, + "ops_per_second": 7.138829481835908, + "sample_size": 36, + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2445, + "runtime": "node" }, { "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 + "mean_ns": 178725141.85714287, + "p50_ns": 178797972, + "p75_ns": 180230857.25, + "p90_ns": 182445355.5, + "p95_ns": 182612111.35, + "p99_ns": 182940545.16, + "min_ns": 178043485, + "max_ns": 183029940, + "std_dev_ns": 499324.82544725196, + "cv": 0.002793814122952932, + "ops_per_second": 5.5951836972064735, + "sample_size": 21, + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2445, + "runtime": "node" }, { "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 + "mean_ns": 1296597720, + "p50_ns": 1297203582, + "p75_ns": 1297308037, + "p90_ns": 1300549918.6, + "p95_ns": 1301630545.8, + "p99_ns": 1302495047.56, + "min_ns": 1292854323, + "max_ns": 1302711173, + "std_dev_ns": 4057316.729119628, + "cv": 0.0031292024245728493, + "ops_per_second": 0.7712492352678207, + "sample_size": 5, + "files_processed": 2446, + "files_total": 2448, + "files_iterated": 2445, + "runtime": "node" }, { "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 + "mean_ns": 1342497528.2, + "p50_ns": 1340425353, + "p75_ns": 1346905511, + "p90_ns": 1350118596.2, + "p95_ns": 1351189624.6, + "p99_ns": 1352046447.32, + "min_ns": 1335012499, + "max_ns": 1352260653, + "std_dev_ns": 7006364.841142459, + "cv": 0.005218903345421041, + "ops_per_second": 0.7448803286370177, + "sample_size": 5, + "files_processed": 2446, + "files_total": 2448, + "files_iterated": 2445, + "runtime": "node" }, { "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, + "mean_ns": 13428895514.5, + "p50_ns": 13434472454, + "p75_ns": 13448460090.5, + "p90_ns": 13483414228.2, + "p95_ns": 13506700293.6, + "p99_ns": 13525329145.92, + "min_ns": 13399688608, + "max_ns": 13529986359, + "std_dev_ns": 19154668.344905008, + "cv": 0.001426377048225786, + "ops_per_second": 0.07446628793263295, "sample_size": 6, - "files_processed": 3855, - "files_total": 4025, - "files_iterated": 3701 + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2446, + "runtime": "node" }, { "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 + "mean_ns": 507668043.5, + "p50_ns": 507537106.5, + "p75_ns": 510123808.25, + "p90_ns": 516586304, + "p95_ns": 519310964, + "p99_ns": 521490692, + "min_ns": 506339768, + "max_ns": 522035624, + "std_dev_ns": 1395815.5141539504, + "cv": 0.00274946499395712, + "ops_per_second": 1.9697911121325091, + "sample_size": 8, + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2446, + "runtime": "node" }, { "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 + "mean_ns": 694161288.1428572, + "p50_ns": 694435813.5, + "p75_ns": 694714348.5, + "p90_ns": 701147903.1, + "p95_ns": 708334588.55, + "p99_ns": 714083936.91, + "min_ns": 692827892, + "max_ns": 715521274, + "std_dev_ns": 706691.9638473869, + "cv": 0.0010180515334383656, + "ops_per_second": 1.4405873924133346, + "sample_size": 7, + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2446, + "runtime": "node" }, { "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 + "mean_ns": 842581054.8, + "p50_ns": 841016016, + "p75_ns": 842934023, + "p90_ns": 845209529, + "p95_ns": 846062717.5, + "p99_ns": 846745268.3, + "min_ns": 840454184, + "max_ns": 846915906, + "std_dev_ns": 2699638.495803318, + "cv": 0.0032040104396177296, + "ops_per_second": 1.1868294383112683, + "sample_size": 5, + "files_processed": 2446, + "files_total": 2448, + "files_iterated": 2446, + "runtime": "node" }, { "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 + "mean_ns": 4493099769.333333, + "p50_ns": 4498314715, + "p75_ns": 7300728081, + "p90_ns": 9612963193.8, + "p95_ns": 10383708231.4, + "p99_ns": 11000304261.48, + "min_ns": 4486677054, + "max_ns": 11154453269, + "std_dev_ns": 5912090.921452437, + "cv": 0.0013158156339648, + "ops_per_second": 0.22256349766040823, + "sample_size": 3, + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2446, + "runtime": "node" + }, + { + "name": "dprint-wasm", + "group": "format/typescript", + "mean_ns": 3104428012.4, + "p50_ns": 3105607188, + "p75_ns": 3109195408, + "p90_ns": 3109704749.2, + "p95_ns": 3109874529.6, + "p99_ns": 3110010353.92, + "min_ns": 3092698333, + "max_ns": 3110044310, + "std_dev_ns": 6951160.321078726, + "cv": 0.0022391114541273766, + "ops_per_second": 0.3221205310626323, + "sample_size": 5, + "files_processed": 2448, + "files_total": 2448, + "files_iterated": 2446, + "runtime": "node" }, { "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 + "mean_ns": 9123909.508840865, + "p50_ns": 9042106, + "p75_ns": 9416333.5, + "p90_ns": 9891497.4, + "p95_ns": 10234852.2, + "p99_ns": 14176169.600000005, + "min_ns": 8752205, + "max_ns": 18981239, + "std_dev_ns": 342912.83917107055, + "cv": 0.03758398073093509, + "ops_per_second": 109.60213919603459, + "sample_size": 509, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" }, { "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 + "mean_ns": 17316347.027027026, + "p50_ns": 17274356, + "p75_ns": 17689193, + "p90_ns": 17922625.6, + "p95_ns": 19570044, + "p99_ns": 20664208.400000002, + "min_ns": 16632722, + "max_ns": 25342203, + "std_dev_ns": 313635.4404078728, + "cv": 0.01811210181445062, + "ops_per_second": 57.74890041411269, + "sample_size": 259, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" }, { "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 + "mean_ns": 18085558.795081966, + "p50_ns": 18020847, + "p75_ns": 18356488, + "p90_ns": 19277685.8, + "p95_ns": 19763633.6, + "p99_ns": 22989714.4, + "min_ns": 17680807, + "max_ns": 23769112, + "std_dev_ns": 317166.320415112, + "cv": 0.0175369931340667, + "ops_per_second": 55.29273445904981, + "sample_size": 244, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" }, { "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 + "mean_ns": 3316126.9743024963, + "p50_ns": 3313593.5, + "p75_ns": 3338725.5, + "p90_ns": 3399061.8000000003, + "p95_ns": 3446014.9, + "p99_ns": 3507148.37, + "min_ns": 3260485, + "max_ns": 3995943, + "std_dev_ns": 26931.138323164356, + "cv": 0.008121262705517773, + "ops_per_second": 301.5566073763918, + "sample_size": 1362, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" }, { "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 + "mean_ns": 4737140.870020964, + "p50_ns": 4726423, + "p75_ns": 4774568, + "p90_ns": 4831878, + "p95_ns": 4860150.8, + "p99_ns": 4913048, + "min_ns": 4658890, + "max_ns": 5058387, + "std_dev_ns": 36121.217938256246, + "cv": 0.007625109518454408, + "ops_per_second": 211.09779663267108, + "sample_size": 954, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" }, { "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 + "mean_ns": 557339230, + "p50_ns": 549537411, + "p75_ns": 565597254, + "p90_ns": 578082432.8, + "p95_ns": 578788740.4, + "p99_ns": 579353786.48, + "min_ns": 539649489, + "max_ns": 579495048, + "std_dev_ns": 14362111.19491702, + "cv": 0.025769065627978528, + "ops_per_second": 1.7942393898954503, + "sample_size": 9, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" }, { "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 + "mean_ns": 6774260.620414673, + "p50_ns": 6765946.5, + "p75_ns": 6830886.5, + "p90_ns": 6995684.8, + "p95_ns": 7064173.8, + "p99_ns": 7342010.549999992, + "min_ns": 6701634, + "max_ns": 10761764, + "std_dev_ns": 47048.678557182546, + "cv": 0.006945212355042601, + "ops_per_second": 147.6175860412626, + "sample_size": 627, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" }, { "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 + "mean_ns": 9738625.60479042, + "p50_ns": 9710363, + "p75_ns": 9816992, + "p90_ns": 9914974.2, + "p95_ns": 9965780, + "p99_ns": 10203037.44, + "min_ns": 9549996, + "max_ns": 13430272, + "std_dev_ns": 103264.4596055366, + "cv": 0.010603596831440047, + "ops_per_second": 102.68389407105876, + "sample_size": 501, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" }, { "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 + "mean_ns": 18918226.36781609, + "p50_ns": 18885129.5, + "p75_ns": 19345143.75, + "p90_ns": 19802674, + "p95_ns": 20013384.15, + "p99_ns": 21136025.340000004, + "min_ns": 17218416, + "max_ns": 21629487, + "std_dev_ns": 621908.927584265, + "cv": 0.0328735324069419, + "ops_per_second": 52.85907783095416, + "sample_size": 261, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" }, { "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 + "mean_ns": 132540457.48387097, + "p50_ns": 133175334.5, + "p75_ns": 142052510.75, + "p90_ns": 179326793, + "p95_ns": 187457932.25, + "p99_ns": 211404413.35, + "min_ns": 123861288, + "max_ns": 212788173, + "std_dev_ns": 9449635.541954929, + "cv": 0.07129623453355644, + "ops_per_second": 7.544866065681804, + "sample_size": 31, + "files_processed": 50, + "files_total": 50, + "files_iterated": 50, + "runtime": "node" } ], - "suppressed_noise": { - "oxfmt::textToDoc": 1, - "panicked at crates/biome_rowan": 1 - } + "suppressed_noise": {}, + "variant_parity": [] } diff --git a/src/routes/docs/benchmarks/benchmarks.ts b/src/routes/docs/benchmarks/benchmarks.ts index e457993..f0cfa95 100644 --- a/src/routes/docs/benchmarks/benchmarks.ts +++ b/src/routes/docs/benchmarks/benchmarks.ts @@ -1,5 +1,5 @@ -import type {BenchmarkBaseline} from './benchmark_data.ts'; +import type { BenchmarkBaseline } from './benchmark_data.ts'; -import json from './benchmarks.json' with {type: 'json'}; +import json from './benchmarks.json' with { type: 'json' }; export const benchmarks_json: BenchmarkBaseline = json as BenchmarkBaseline; diff --git a/src/routes/docs/benchmarks/benchmarks_cli.ts b/src/routes/docs/benchmarks/benchmarks_cli.ts new file mode 100644 index 0000000..1aead10 --- /dev/null +++ b/src/routes/docs/benchmarks/benchmarks_cli.ts @@ -0,0 +1,144 @@ +// The independent end-to-end CLI benchmark — a fork of Oxc's official +// `bench-formatter` that adds tsv (https://github.com/ryanatkn/oxc-bench-formatter, +// forked from https://github.com/oxc-project/bench-formatter — `tsv` is the fork's +// DEFAULT branch, so the bare fork URL already lands on it; upstream carries +// neither tsv nor this analysis). Unlike the in-process, single-threaded numbers +// elsewhere on this page, it measures the WHOLE CLI: process spawn, file +// discovery, I/O, and each tool's default multi-file parallelism. tsv, oxfmt, and +// biome parallelize across files while prettier is effectively serial, so the +// wall-clock ratios scale with core count and are machine-dependent — the +// parallelism-neutral view is CPU work (hyperfine `User` time). tsv runs only in +// the JSX-free scenarios (it has no JSX/TSX parser). +// +// The numbers come from `benchmarks_formatters.json`, generated from that +// harness's README by `benchmarks_formatters.gen.json.ts`; only the prose below +// is authored here. To refresh: run `pnpm run update-readme` in the harness, then +// `gro gen` here. + +import { benchmarks_formatters_json } from './benchmarks_formatters.ts'; +import type { FormatterScenario } from './formatter_benchmark_data.ts'; + +export interface CliFormatterResult { + /** Display label; `tsv` is the reference row every ratio is computed against. */ + label: string; + /** hyperfine wall-clock mean, in milliseconds — what you experience typing the command. */ + wall_ms: number; + /** hyperfine `User` time (total CPU across all threads), in ms — the parallelism-neutral view. */ + cpu_ms: number; + /** Peak resident set size (RSS), in megabytes; `null` when the harness measured no memory. */ + memory_mb: number | null; +} + +export interface CliScenario { + key: string; + heading: string; + /** One-line description of the corpus and what makes the comparison fair. */ + description: string; + /** Whether every formatter is effectively single-threaded here (one input file). */ + single_threaded: boolean; + /** Results ascending by wall-clock time, tsv-relative ratios computed by the component. */ + results: Array; +} + +export interface BenchmarksCliReport { + machine: string; + versions: Record; + scenarios: Array; +} + +/** + * The prose framing for each scenario, keyed by its generated scenario id — the + * harness measures the numbers but doesn't explain them. Also fixes the page + * order, which is by narrative weight (the multi-file repo leads), not the order + * the harness runs them in. A scenario missing from here is dropped rather than + * rendered unexplained. + */ +const SCENARIO_COPY: Record> = { + 'typescript-only-tsv-fair': { + heading: 'TypeScript repo (Outline, non-JSX subset)', + description: + "Outline's .ts/.js/.mjs files — the common set every formatter supports, each scoped to it, with a preflight parse check confirming none reject anything. The fair way to put tsv on a real multi-file repo.", + single_threaded: false + }, + 'large-single-file': { + heading: 'Large single file (TypeScript compiler parser.ts, ~540KB)', + description: + 'One ~13.7K-line .ts file. With a single input every formatter is effectively single-threaded, so wall-clock is close to an engine comparison here — except oxfmt, which spins up a worker pool it cannot use, inflating its CPU time.', + single_threaded: true + } +}; + +/** + * Display labels for the harness's hyperfine command names, which read better + * spaced out in a table. Names absent here are displayed as-is. + */ +export const CLI_LABELS: Record = { + 'prettier+oxc-parser': 'prettier + oxc-parser' +}; + +const to_results = (scenario: FormatterScenario): Array => + scenario.timings + .map((timing) => ({ + label: CLI_LABELS[timing.name] ?? timing.name, + wall_ms: timing.mean_ms, + cpu_ms: timing.user_ms, + memory_mb: scenario.memory.find((m) => m.name === timing.name)?.mean_mb ?? null + })) + .sort((a, b) => a.wall_ms - b.wall_ms); + +/** The scenarios the page has prose for; every one must resolve to generated data. */ +export const CLI_SCENARIO_KEYS = Object.keys(SCENARIO_COPY); + +const to_scenarios = (): Array => + Object.entries(SCENARIO_COPY).flatMap(([key, copy]) => { + const scenario = benchmarks_formatters_json.scenarios.find((s) => s.id === key); + return scenario ? [{ key, ...copy, results: to_results(scenario) }] : []; + }); + +export const benchmarks_cli: BenchmarksCliReport = { + machine: benchmarks_formatters_json.machine, + versions: benchmarks_formatters_json.versions, + scenarios: to_scenarios() +}; + +/** The scenario id the page's prose calls "the TypeScript repo" — the multi-file, real-repo run. */ +export const CLI_TS_REPO_KEY = 'typescript-only-tsv-fair'; + +/** + * How many times faster or lighter tsv is than `label` in one CLI scenario, by + * the given metric — the ratios the page's prose quotes. + * + * @returns the ratio, or `undefined` when the scenario, the formatter, or either + * side's measurement is absent + */ +export const cli_speedup_vs_tsv = ( + scenario_key: string, + label: string, + metric: keyof Omit +): number | undefined => { + const results = benchmarks_cli.scenarios.find((s) => s.key === scenario_key)?.results; + const tsv = results?.find((r) => r.label === 'tsv')?.[metric]; + const other = results?.find((r) => r.label === label)?.[metric]; + if (tsv == null || other == null || !tsv) return undefined; + return other / tsv; +}; + +/** + * The span of "times less memory than tsv" across every non-tsv row, over one + * scenario or all of them — the prose's `3–13x` claim. + * + * @returns the low and high ratio, or `undefined` when nothing was measured + */ +export const cli_memory_ratio_range = ( + scenario_key?: string +): { min: number; max: number } | undefined => { + const scenarios = benchmarks_cli.scenarios.filter((s) => !scenario_key || s.key === scenario_key); + const ratios = scenarios.flatMap((scenario) => + scenario.results + .filter((r) => r.label !== 'tsv') + .map((r) => cli_speedup_vs_tsv(scenario.key, r.label, 'memory_mb')) + .filter((ratio) => ratio !== undefined) + ); + if (ratios.length === 0) return undefined; + return { min: Math.min(...ratios), max: Math.max(...ratios) }; +}; diff --git a/src/routes/docs/benchmarks/benchmarks_conformance.json b/src/routes/docs/benchmarks/benchmarks_conformance.json new file mode 100644 index 0000000..82322b0 --- /dev/null +++ b/src/routes/docs/benchmarks/benchmarks_conformance.json @@ -0,0 +1,650 @@ +{ + "version": 7, + "runtime": "node", + "corpus_kind": "conformance", + "timestamp": "2026-07-22T09:27:46.776Z", + "git_commit": "d063479a", + "machine": { + "cpu_model": "AMD Ryzen 5 PRO 7530U with Radeon Graphics", + "os": "linux", + "arch": "x86_64", + "runtime_version": "24.14.1" + }, + "corpus": { + "svelte": 4539, + "typescript": 44224, + "css": 22641 + }, + "corpus_sources": [ + { + "path": "../prettier-plugin-svelte/test", + "files": 318, + "by_language": { + "svelte": 318, + "typescript": 0, + "css": 0 + }, + "repo": { + "url": "https://github.com/sveltejs/prettier-plugin-svelte", + "slug": "sveltejs/prettier-plugin-svelte", + "commit": "7809486a9716faa2234c8a45d88b601034de52d8", + "subpath": "test" + } + }, + { + "path": "../prettier/tests/format/typescript", + "files": 793, + "by_language": { + "svelte": 0, + "typescript": 793, + "css": 0 + }, + "repo": { + "url": "https://github.com/prettier/prettier", + "slug": "prettier/prettier", + "commit": "1dcd0b05d026356cce5bf538c6d425955b29903b", + "subpath": "tests/format/typescript" + } + }, + { + "path": "../prettier/tests/format/js", + "files": 1103, + "by_language": { + "svelte": 0, + "typescript": 1103, + "css": 0 + }, + "repo": { + "url": "https://github.com/prettier/prettier", + "slug": "prettier/prettier", + "commit": "1dcd0b05d026356cce5bf538c6d425955b29903b", + "subpath": "tests/format/js" + } + }, + { + "path": "../prettier/tests/format/css", + "files": 228, + "by_language": { + "svelte": 0, + "typescript": 78, + "css": 150 + }, + "repo": { + "url": "https://github.com/prettier/prettier", + "slug": "prettier/prettier", + "commit": "1dcd0b05d026356cce5bf538c6d425955b29903b", + "subpath": "tests/format/css" + } + }, + { + "path": "../prettier/tests/format/html", + "files": 84, + "by_language": { + "svelte": 84, + "typescript": 0, + "css": 0 + }, + "repo": { + "url": "https://github.com/prettier/prettier", + "slug": "prettier/prettier", + "commit": "1dcd0b05d026356cce5bf538c6d425955b29903b", + "subpath": "tests/format/html" + } + }, + { + "path": "../svelte/packages/svelte/tests", + "files": 4455, + "by_language": { + "svelte": 4137, + "typescript": 137, + "css": 181 + }, + "repo": { + "url": "https://github.com/sveltejs/svelte", + "slug": "sveltejs/svelte", + "commit": "b4d1583ae20f3869a88a731d9a265c546c099f66", + "subpath": "packages/svelte/tests" + } + }, + { + "path": "benches/js/.cache/wpt_css", + "files": 22310, + "by_language": { + "svelte": 0, + "typescript": 0, + "css": 22310 + }, + "repo": { + "url": "https://github.com/web-platform-tests/wpt", + "slug": "web-platform-tests/wpt", + "commit": "", + "subpath": "" + } + }, + { + "path": "benches/js/.cache/test262_files.json", + "files": 42113, + "by_language": { + "svelte": 0, + "typescript": 42113, + "css": 0 + }, + "repo": { + "url": "https://github.com/tc39/test262", + "slug": "tc39/test262", + "commit": "", + "subpath": "" + } + } + ], + "versions": { + "tsv": "0.2.0", + "svelte": "5.56.4", + "acorn": "8.16.0", + "acorn_ts": "1.0.11", + "prettier": "3.9.5", + "prettier_svelte": "4.1.1", + "oxc_parser": "0.140.0", + "oxfmt": "0.60.0", + "biome": "2.5.4", + "dprint": "0.96.1" + }, + "binary_sizes": [ + { + "label": "tsv (ffi)", + "bytes": 3346544, + "kind": "native", + "gzip_bytes": 1442507 + }, + { + "label": "tsv format (ffi)", + "bytes": 3095968, + "kind": "native", + "gzip_bytes": 1339168 + }, + { + "label": "tsv parse (ffi)", + "bytes": 1606912, + "kind": "native", + "gzip_bytes": 703248 + }, + { + "label": "tsv (napi)", + "bytes": 3479392, + "kind": "native", + "gzip_bytes": 1486322 + }, + { + "label": "tsv_format_wasm", + "bytes": 2216330, + "kind": "wasm", + "gzip_bytes": 787915 + }, + { + "label": "tsv_parse_wasm", + "bytes": 1052601, + "kind": "wasm", + "gzip_bytes": 388661 + }, + { + "label": "tsv_wasm", + "bytes": 2444292, + "kind": "wasm", + "gzip_bytes": 867803 + }, + { + "label": "biome (wasm)", + "bytes": 38584187, + "kind": "wasm", + "gzip_bytes": 9281487 + }, + { + "label": "dprint (wasm)", + "bytes": 4174185, + "kind": "wasm", + "gzip_bytes": 1155127 + }, + { + "label": "oxc-parser (napi)", + "bytes": 2354904, + "kind": "native", + "gzip_bytes": 954754 + }, + { + "label": "oxfmt (napi)", + "bytes": 8844056, + "kind": "native", + "gzip_bytes": 3575335 + }, + { + "label": "oxc-parser (wasm)", + "bytes": 1531455, + "kind": "wasm", + "gzip_bytes": 495227 + } + ], + "entries": [ + { + "name": "svelte/compiler", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 4539, + "files_total": 4539, + "files_iterated": 4539, + "runtime": "node" + }, + { + "name": "tsv-json", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 4539, + "files_total": 4539, + "files_iterated": 4539, + "runtime": "node" + }, + { + "name": "tsv-json-no-locations", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 4539, + "files_total": 4539, + "files_iterated": 4539, + "runtime": "node" + }, + { + "name": "tsv_wasm-json", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 4539, + "files_total": 4539, + "files_iterated": 4539, + "runtime": "node" + }, + { + "name": "tsv_wasm-json-no-locations", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 4539, + "files_total": 4539, + "files_iterated": 4539, + "runtime": "node" + }, + { + "name": "tsv-internal", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 4539, + "files_total": 4539, + "files_iterated": 4539, + "runtime": "node" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 4539, + "files_total": 4539, + "files_iterated": 4539, + "runtime": "node" + }, + { + "name": "acorn-typescript", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 43647, + "files_total": 44224, + "files_iterated": 43619, + "runtime": "node" + }, + { + "name": "tsv-json", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 44023, + "files_total": 44224, + "files_iterated": 43619, + "runtime": "node" + }, + { + "name": "tsv-json-no-locations", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 44023, + "files_total": 44224, + "files_iterated": 43619, + "runtime": "node" + }, + { + "name": "tsv_wasm-json", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 44023, + "files_total": 44224, + "files_iterated": 43619, + "runtime": "node" + }, + { + "name": "tsv_wasm-json-no-locations", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 44023, + "files_total": 44224, + "files_iterated": 43619, + "runtime": "node" + }, + { + "name": "tsv-internal", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 44023, + "files_total": 44224, + "files_iterated": 43619, + "runtime": "node" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 44023, + "files_total": 44224, + "files_iterated": 43619, + "runtime": "node" + }, + { + "name": "oxc-parser", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 44014, + "files_total": 44224, + "files_iterated": 43619, + "runtime": "node" + }, + { + "name": "oxc-parser-wasm", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 44014, + "files_total": 44224, + "files_iterated": 43619, + "runtime": "node" + }, + { + "name": "svelte/compiler", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 22402, + "files_total": 22641, + "files_iterated": 22388, + "runtime": "node" + }, + { + "name": "tsv-json", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 22457, + "files_total": 22641, + "files_iterated": 22388, + "runtime": "node" + }, + { + "name": "tsv_wasm-json", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 22457, + "files_total": 22641, + "files_iterated": 22388, + "runtime": "node" + }, + { + "name": "tsv-internal", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 22457, + "files_total": 22641, + "files_iterated": 22388, + "runtime": "node" + }, + { + "name": "tsv_wasm-internal", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "files_processed": 22457, + "files_total": 22641, + "files_iterated": 22388, + "runtime": "node" + } + ], + "suppressed_noise": {}, + "variant_parity": [] +} diff --git a/src/routes/docs/benchmarks/benchmarks_conformance.ts b/src/routes/docs/benchmarks/benchmarks_conformance.ts new file mode 100644 index 0000000..cf47f5d --- /dev/null +++ b/src/routes/docs/benchmarks/benchmarks_conformance.ts @@ -0,0 +1,5 @@ +import type { BenchmarkBaseline } from './benchmark_data.ts'; + +import json from './benchmarks_conformance.json' with { type: 'json' }; + +export const benchmarks_conformance_json: BenchmarkBaseline = json as BenchmarkBaseline; diff --git a/src/routes/docs/benchmarks/benchmarks_cross_runtime.json b/src/routes/docs/benchmarks/benchmarks_cross_runtime.json new file mode 100644 index 0000000..52ab6b6 --- /dev/null +++ b/src/routes/docs/benchmarks/benchmarks_cross_runtime.json @@ -0,0 +1,739 @@ +{ + "version": 7, + "kind": "combined", + "generated": "2026-07-22T09:27:01.099Z", + "runtimes": ["deno", "node", "bun"], + "mixed_vintage": false, + "mixed_machine": false, + "sources": [ + { + "runtime": "deno", + "timestamp": "2026-07-22T09:08:34.891Z", + "git_commit": "d063479a", + "tsv": "0.2.0", + "machine": { + "cpu_model": "AMD Ryzen 5 PRO 7530U with Radeon Graphics", + "os": "linux", + "arch": "x86_64", + "runtime_version": "2.9.3" + } + }, + { + "runtime": "node", + "timestamp": "2026-07-22T09:18:34.995Z", + "git_commit": "d063479a", + "tsv": "0.2.0", + "machine": { + "cpu_model": "AMD Ryzen 5 PRO 7530U with Radeon Graphics", + "os": "linux", + "arch": "x86_64", + "runtime_version": "24.14.1" + } + }, + { + "runtime": "bun", + "timestamp": "2026-07-22T09:27:00.825Z", + "git_commit": "d063479a", + "tsv": "0.2.0", + "machine": { + "cpu_model": "AMD Ryzen 5 PRO 7530U with Radeon Graphics", + "os": "linux", + "arch": "x86_64", + "runtime_version": "1.3.14" + } + } + ], + "rows": [ + { + "group": "parse/svelte", + "name": "svelte/compiler", + "ops_per_second": { + "deno": 2.3234362309765233, + "node": 2.2216431647548864, + "bun": 1.4131199693476677 + }, + "mean_ns": { + "deno": 430397007.1, + "node": 450117289.7, + "bun": 707654000.8571428 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "parse/svelte", + "name": "tsv-json", + "ops_per_second": { + "deno": 5.03910728545968, + "node": 4.6499867822902035, + "bun": 6.022114301293246 + }, + "mean_ns": { + "deno": 198447848.66666666, + "node": 215054374.7368421, + "bun": 166054636.29032257 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "parse/svelte", + "name": "tsv-json-no-locations", + "ops_per_second": { + "deno": 7.866195242246116, + "node": 7.302420321988621, + "bun": 8.425385240870403 + }, + "mean_ns": { + "deno": 127126262.34210527, + "node": 136940898.48387095, + "bun": 118688934.85714285 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "parse/svelte", + "name": "tsv_wasm-json", + "ops_per_second": { + "deno": 4.11280382829322, + "node": 4.16734009656939, + "bun": 5.599466083525936 + }, + "mean_ns": { + "deno": 243143131, + "node": 239961216.70588234, + "bun": 178588455.5925926 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "parse/svelte", + "name": "tsv_wasm-json-no-locations", + "ops_per_second": { + "deno": 6.26423723490975, + "node": 6.445842455503006, + "bun": 7.629741637830749 + }, + "mean_ns": { + "deno": 159636355.15384614, + "node": 155138759.1153846, + "bun": 131066037.02564102 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "parse/svelte", + "name": "tsv-internal", + "ops_per_second": { + "deno": 50.884877826102425, + "node": 49.18726473245663, + "bun": 52.797052562303065 + }, + "mean_ns": { + "deno": 19652204.00877193, + "node": 20330465.730088495, + "bun": 18940451.24621212 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "parse/svelte", + "name": "tsv_wasm-internal", + "ops_per_second": { + "deno": 32.22839602213156, + "node": 36.1862196566141, + "bun": 37.6696281621773 + }, + "mean_ns": { + "deno": 31028537.669491526, + "node": 27634829.2109375, + "bun": 26546585.373626374 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "format/svelte", + "name": "prettier", + "ops_per_second": { + "deno": 0.23714053845257216, + "node": 0.22770016339498617, + "bun": 0.2587928860879906 + }, + "mean_ns": { + "deno": 4216908701.1666665, + "node": 4391740370.714286, + "bun": 3864093851.714286 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "format/svelte", + "name": "tsv", + "ops_per_second": { + "deno": 14.710947545190182, + "node": 14.703406947264233, + "bun": 14.469091128435753 + }, + "mean_ns": { + "deno": 67976586.61538461, + "node": 68011448.20289855, + "bun": 69112841.375 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "format/svelte", + "name": "tsv_wasm", + "ops_per_second": { + "deno": 9.340617577022751, + "node": 10.276576472095927, + "bun": 10.522953526889031 + }, + "mean_ns": { + "deno": 107059302.20930232, + "node": 97308671.10416667, + "bun": 95030354.11538461 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "format/svelte", + "name": "oxfmt", + "ops_per_second": { + "deno": 0.23743594970499868, + "node": 0.23101750520591594, + "bun": 0.19643263399368738 + }, + "mean_ns": { + "deno": 4211662139.8, + "node": 4328676301.428572, + "bun": 5090803802.142858 + }, + "files_iterated": { + "deno": 767, + "node": 767, + "bun": 767 + } + }, + { + "group": "format/svelte", + "name": "biome-wasm", + "ops_per_second": { + "deno": 1.4068179347357905, + "node": 1.1005294297485004 + }, + "mean_ns": { + "deno": 710824034.375, + "node": 908653574.3333334 + }, + "files_iterated": { + "deno": 767, + "node": 767 + } + }, + { + "group": "parse/typescript", + "name": "acorn-typescript", + "ops_per_second": { + "deno": 0.35017861058934563, + "node": 0.319602416005853, + "bun": 0.14577892405595072 + }, + "mean_ns": { + "deno": 2855685555.2, + "node": 3128887486.2, + "bun": 6859702158.428572 + }, + "files_iterated": { + "deno": 2445, + "node": 2445, + "bun": 2445 + } + }, + { + "group": "parse/typescript", + "name": "tsv-json", + "ops_per_second": { + "deno": 0.5530626912278549, + "node": 0.49114251487265487, + "bun": 0.7188311660438776 + }, + "mean_ns": { + "deno": 1808113286, + "node": 2036068900, + "bun": 1391147250.2 + }, + "files_iterated": { + "deno": 2445, + "node": 2445, + "bun": 2445 + } + }, + { + "group": "parse/typescript", + "name": "tsv-json-no-locations", + "ops_per_second": { + "deno": 1.1423054195223503, + "node": 0.9939125300571001, + "bun": 1.2560381326014052 + }, + "mean_ns": { + "deno": 875422617.2, + "node": 1006124754.2, + "bun": 796154172.4285715 + }, + "files_iterated": { + "deno": 2445, + "node": 2445, + "bun": 2445 + } + }, + { + "group": "parse/typescript", + "name": "tsv_wasm-json", + "ops_per_second": { + "deno": 0.474850274197908, + "node": 0.46275614691458916, + "bun": 0.6751595659121256 + }, + "mean_ns": { + "deno": 2105926971.8, + "node": 2160965352.2, + "bun": 1481131351 + }, + "files_iterated": { + "deno": 2445, + "node": 2445, + "bun": 2445 + } + }, + { + "group": "parse/typescript", + "name": "tsv_wasm-json-no-locations", + "ops_per_second": { + "deno": 0.933349493315349, + "node": 0.9159845138907174, + "bun": 1.1532055304323752 + }, + "mean_ns": { + "deno": 1071410020.75, + "node": 1091721513.6666667, + "bun": 867148113.3333334 + }, + "files_iterated": { + "deno": 2445, + "node": 2445, + "bun": 2445 + } + }, + { + "group": "parse/typescript", + "name": "tsv-internal", + "ops_per_second": { + "deno": 7.63397113225453, + "node": 7.138829481835908, + "bun": 8.284841871605634 + }, + "mean_ns": { + "deno": 130993421.72972973, + "node": 140078986.69444445, + "bun": 120702364.08823529 + }, + "files_iterated": { + "deno": 2445, + "node": 2445, + "bun": 2445 + } + }, + { + "group": "parse/typescript", + "name": "tsv_wasm-internal", + "ops_per_second": { + "deno": 4.961691948931678, + "node": 5.5951836972064735, + "bun": 5.9011458444952325 + }, + "mean_ns": { + "deno": 201544152.73913044, + "node": 178725141.85714287, + "bun": 169458614.70833334 + }, + "files_iterated": { + "deno": 2445, + "node": 2445, + "bun": 2445 + } + }, + { + "group": "parse/typescript", + "name": "oxc-parser", + "ops_per_second": { + "deno": 0.8635787815927837, + "node": 0.7712492352678207, + "bun": 1.0291052890541648 + }, + "mean_ns": { + "deno": 1157971943.4, + "node": 1296597720, + "bun": 971717870.5 + }, + "files_iterated": { + "deno": 2445, + "node": 2445, + "bun": 2445 + } + }, + { + "group": "parse/typescript", + "name": "oxc-parser-wasm", + "ops_per_second": { + "deno": 0.7875832431777169, + "node": 0.7448803286370177 + }, + "mean_ns": { + "deno": 1269707054.6666667, + "node": 1342497528.2 + }, + "files_iterated": { + "deno": 2445, + "node": 2445 + } + }, + { + "group": "format/typescript", + "name": "prettier", + "ops_per_second": { + "deno": 0.08232064665797907, + "node": 0.07446628793263295, + "bun": 0.06408892677184211 + }, + "mean_ns": { + "deno": 12147620804.714285, + "node": 13428895514.5, + "bun": 15603319487 + }, + "files_iterated": { + "deno": 2446, + "node": 2446, + "bun": 2446 + } + }, + { + "group": "format/typescript", + "name": "tsv", + "ops_per_second": { + "deno": 2.0016063010662433, + "node": 1.9697911121325091, + "bun": 1.9487498480140761 + }, + "mean_ns": { + "deno": 499598747, + "node": 507668043.5, + "bun": 513149494.8 + }, + "files_iterated": { + "deno": 2446, + "node": 2446, + "bun": 2446 + } + }, + { + "group": "format/typescript", + "name": "tsv_wasm", + "ops_per_second": { + "deno": 1.2977692582359848, + "node": 1.4405873924133346, + "bun": 1.4775642868780696 + }, + "mean_ns": { + "deno": 770553003.6666666, + "node": 694161288.1428572, + "bun": 676789503.4285715 + }, + "files_iterated": { + "deno": 2446, + "node": 2446, + "bun": 2446 + } + }, + { + "group": "format/typescript", + "name": "oxfmt", + "ops_per_second": { + "deno": 1.1953372949795649, + "node": 1.1868294383112683, + "bun": 0.9657736954840443 + }, + "mean_ns": { + "deno": 836583953.5, + "node": 842581054.8, + "bun": 1035439259.4 + }, + "files_iterated": { + "deno": 2446, + "node": 2446, + "bun": 2446 + } + }, + { + "group": "format/typescript", + "name": "biome-wasm", + "ops_per_second": { + "deno": 0.24161617382436792, + "node": 0.22256349766040823 + }, + "mean_ns": { + "deno": 4138795777.5, + "node": 4493099769.333333 + }, + "files_iterated": { + "deno": 2446, + "node": 2446 + } + }, + { + "group": "format/typescript", + "name": "dprint-wasm", + "ops_per_second": { + "deno": 0.2939344856052669, + "node": 0.3221205310626323, + "bun": 0.3360723021627639 + }, + "mean_ns": { + "deno": 3402118665.8, + "node": 3104428012.4, + "bun": 2975550182.4 + }, + "files_iterated": { + "deno": 2446, + "node": 2446, + "bun": 2446 + } + }, + { + "group": "parse/css", + "name": "svelte/compiler", + "ops_per_second": { + "deno": 96.83747985855102, + "node": 109.60213919603459, + "bun": 68.1549019339242 + }, + "mean_ns": { + "deno": 10326580.17805383, + "node": 9123909.508840865, + "bun": 14672458.93728223 + }, + "files_iterated": { + "deno": 50, + "node": 50, + "bun": 50 + } + }, + { + "group": "parse/css", + "name": "tsv-json", + "ops_per_second": { + "deno": 67.15927625563137, + "node": 57.74890041411269, + "bun": 74.45528345165681 + }, + "mean_ns": { + "deno": 14889975.826923076, + "node": 17316347.027027026, + "bun": 13430880.303468209 + }, + "files_iterated": { + "deno": 50, + "node": 50, + "bun": 50 + } + }, + { + "group": "parse/css", + "name": "tsv_wasm-json", + "ops_per_second": { + "deno": 52.00600032417462, + "node": 55.29273445904981, + "bun": 69.69675930034147 + }, + "mean_ns": { + "deno": 19228550.432, + "node": 18085558.795081966, + "bun": 14347869.399361022 + }, + "files_iterated": { + "deno": 50, + "node": 50, + "bun": 50 + } + }, + { + "group": "parse/css", + "name": "tsv-internal", + "ops_per_second": { + "deno": 316.0234093250357, + "node": 301.5566073763918, + "bun": 343.2247565755417 + }, + "mean_ns": { + "deno": 3164322.5485599465, + "node": 3316126.9743024963, + "bun": 2913542.746675111 + }, + "files_iterated": { + "deno": 50, + "node": 50, + "bun": 50 + } + }, + { + "group": "parse/css", + "name": "tsv_wasm-internal", + "ops_per_second": { + "deno": 178.49939360958777, + "node": 211.09779663267108, + "bun": 218.36861786361445 + }, + "mean_ns": { + "deno": 5602259.92804878, + "node": 4737140.870020964, + "bun": 4579412.59959142 + }, + "files_iterated": { + "deno": 50, + "node": 50, + "bun": 50 + } + }, + { + "group": "format/css", + "name": "prettier", + "ops_per_second": { + "deno": 1.9453387285461383, + "node": 1.7942393898954503, + "bun": 1.9775382167053115 + }, + "mean_ns": { + "deno": 514049294, + "node": 557339230, + "bun": 505679228.625 + }, + "files_iterated": { + "deno": 50, + "node": 50, + "bun": 50 + } + }, + { + "group": "format/css", + "name": "tsv", + "ops_per_second": { + "deno": 153.9639336807719, + "node": 147.6175860412626, + "bun": 153.79399297527175 + }, + "mean_ns": { + "deno": 6495027.608695652, + "node": 6774260.620414673, + "bun": 6502204.544235925 + }, + "files_iterated": { + "deno": 50, + "node": 50, + "bun": 50 + } + }, + { + "group": "format/css", + "name": "tsv_wasm", + "ops_per_second": { + "deno": 88.06396381287563, + "node": 102.68389407105876, + "bun": 104.09990571889091 + }, + "mean_ns": { + "deno": 11355382.573113207, + "node": 9738625.60479042, + "bun": 9606156.634765625 + }, + "files_iterated": { + "deno": 50, + "node": 50, + "bun": 50 + } + }, + { + "group": "format/css", + "name": "oxfmt", + "ops_per_second": { + "deno": 55.80449913414042, + "node": 52.85907783095416, + "bun": 46.70127981781842 + }, + "mean_ns": { + "deno": 17919702.0942029, + "node": 18918226.36781609, + "bun": 21412689.414529916 + }, + "files_iterated": { + "deno": 50, + "node": 50, + "bun": 50 + } + }, + { + "group": "format/css", + "name": "biome-wasm", + "ops_per_second": { + "deno": 10.13362212121382, + "node": 7.544866065681804 + }, + "mean_ns": { + "deno": 98681398.2244898, + "node": 132540457.48387097 + }, + "files_iterated": { + "deno": 50, + "node": 50 + } + } + ] +} diff --git a/src/routes/docs/benchmarks/benchmarks_cross_runtime.ts b/src/routes/docs/benchmarks/benchmarks_cross_runtime.ts new file mode 100644 index 0000000..3793a17 --- /dev/null +++ b/src/routes/docs/benchmarks/benchmarks_cross_runtime.ts @@ -0,0 +1,5 @@ +import type { CrossRuntimeReport } from './benchmark_data.ts'; + +import json from './benchmarks_cross_runtime.json' with { type: 'json' }; + +export const benchmarks_cross_runtime_json: CrossRuntimeReport = json as CrossRuntimeReport; diff --git a/src/routes/docs/benchmarks/benchmarks_formatters.gen.json.ts b/src/routes/docs/benchmarks/benchmarks_formatters.gen.json.ts new file mode 100644 index 0000000..6b91b5f --- /dev/null +++ b/src/routes/docs/benchmarks/benchmarks_formatters.gen.json.ts @@ -0,0 +1,40 @@ +import type { Gen } from '@fuzdev/gro/gen.ts'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +import { parse_formatter_benchmarks } from './formatter_benchmark_data.ts'; + +// The formatter comparison harness lives in a sibling checkout and publishes its +// results only as prose — see `formatter_benchmark_data.ts`. +const README_PATH = '../oxc-bench-formatter/README.md'; + +/** + * Generate `benchmarks_formatters.json` from the sibling formatter-benchmark + * harness's README. + * + * A MISSING sibling checkout is the one tolerated case — it's an optional repo, + * so generation is skipped, the committed JSON stands, and `gro gen --check` + * passes on a machine (or CI) that has only this repo. A README that IS present + * but doesn't parse fails the task instead: the alternative is quietly publishing + * stale or scenario-stripped numbers. + */ +export const gen: Gen = { + generate: async ({ log }) => { + const path = resolve(README_PATH); + + let readme; + try { + readme = await readFile(path, 'utf8'); + } catch (error) { + // only "it isn't there" is benign; an unreadable file is a real problem + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + log.info(`skipping formatter benchmarks, no readme at ${path}`); + return null; + } + + const benchmarks = parse_formatter_benchmarks(readme); + log.info(`parsed ${benchmarks.scenarios.length} tsv scenario(s) from ${path}`); + return JSON.stringify(benchmarks); + }, + dependencies: { files: [resolve(README_PATH)] } +}; diff --git a/src/routes/docs/benchmarks/benchmarks_formatters.json b/src/routes/docs/benchmarks/benchmarks_formatters.json new file mode 100644 index 0000000..c37f10b --- /dev/null +++ b/src/routes/docs/benchmarks/benchmarks_formatters.json @@ -0,0 +1,299 @@ +{ + "machine": "AMD Ryzen 5 PRO 7530U with Radeon Graphics · 12 threads · linux x64", + "versions": { + "prettier": "3.9.5", + "biome": "2.5.4", + "oxfmt": "0.60.0", + "tsv": "0.1.0" + }, + "scenarios": [ + { + "id": "large-single-file", + "name": "Large Single File", + "target": "TypeScript compiler parser.ts (~540KB)", + "warmup_runs": 2, + "benchmark_runs": 5, + "preflight": [ + { + "name": "prettier", + "rejected": 0, + "unavailable": false + }, + { + "name": "prettier+oxc-parser", + "rejected": 0, + "unavailable": false + }, + { + "name": "biome", + "rejected": 0, + "unavailable": false + }, + { + "name": "oxfmt", + "rejected": 0, + "unavailable": false + }, + { + "name": "tsv", + "rejected": 0, + "unavailable": false + } + ], + "timings": [ + { + "name": "prettier", + "mean_ms": 1605, + "stddev_ms": 22, + "user_ms": 1541, + "system_ms": 858, + "min_ms": 1572, + "max_ms": 1623 + }, + { + "name": "prettier+oxc-parser", + "mean_ms": 985, + "stddev_ms": 4.8, + "user_ms": 889, + "system_ms": 462.3, + "min_ms": 977.6, + "max_ms": 990 + }, + { + "name": "biome", + "mean_ms": 113.9, + "stddev_ms": 1.7, + "user_ms": 87.1, + "system_ms": 34.6, + "min_ms": 112.2, + "max_ms": 116.7 + }, + { + "name": "oxfmt", + "mean_ms": 195.8, + "stddev_ms": 9.4, + "user_ms": 354.4, + "system_ms": 201.4, + "min_ms": 188.1, + "max_ms": 211.9 + }, + { + "name": "tsv", + "mean_ms": 23.8, + "stddev_ms": 0.2, + "user_ms": 14.9, + "system_ms": 8.8, + "min_ms": 23.6, + "max_ms": 24.1 + } + ], + "baseline": "tsv", + "speedups": [ + { + "name": "biome", + "ratio": 4.79, + "ratio_stddev": 0.08 + }, + { + "name": "oxfmt", + "ratio": 8.24, + "ratio_stddev": 0.4 + }, + { + "name": "prettier+oxc-parser", + "ratio": 41.44, + "ratio_stddev": 0.43 + }, + { + "name": "prettier", + "ratio": 67.55, + "ratio_stddev": 1.13 + } + ], + "memory": [ + { + "name": "prettier", + "mean_mb": 305.9, + "min_mb": 293.4, + "max_mb": 313.3, + "ratio": 12.34, + "ratio_stddev": 0.44 + }, + { + "name": "prettier+oxc-parser", + "mean_mb": 196.2, + "min_mb": 194.6, + "max_mb": 197.9, + "ratio": 7.91, + "ratio_stddev": 0.19 + }, + { + "name": "biome", + "mean_mb": 101, + "min_mb": 98, + "max_mb": 104, + "ratio": 4.07, + "ratio_stddev": 0.13 + }, + { + "name": "oxfmt", + "mean_mb": 118.6, + "min_mb": 118.6, + "max_mb": 118.7, + "ratio": 4.79, + "ratio_stddev": 0.11 + }, + { + "name": "tsv", + "mean_mb": 24.8, + "min_mb": 24.2, + "max_mb": 25.6 + } + ] + }, + { + "id": "typescript-only-tsv-fair", + "name": "TypeScript-only (tsv-fair)", + "target": "Outline repository (non-JSX JS/TS subset)", + "warmup_runs": 2, + "benchmark_runs": 5, + "preflight": [ + { + "name": "prettier", + "rejected": 0, + "unavailable": false + }, + { + "name": "prettier+oxc-parser", + "rejected": 0, + "unavailable": false + }, + { + "name": "biome", + "rejected": 0, + "unavailable": false + }, + { + "name": "oxfmt", + "rejected": 0, + "unavailable": false + }, + { + "name": "tsv", + "rejected": 0, + "unavailable": false + } + ], + "timings": [ + { + "name": "prettier", + "mean_ms": 8250, + "stddev_ms": 118, + "user_ms": 13524, + "system_ms": 882, + "min_ms": 8121, + "max_ms": 8382 + }, + { + "name": "prettier+oxc-parser", + "mean_ms": 6740, + "stddev_ms": 24, + "user_ms": 8533, + "system_ms": 560, + "min_ms": 6710, + "max_ms": 6768 + }, + { + "name": "biome", + "mean_ms": 361.6, + "stddev_ms": 1.4, + "user_ms": 2510.6, + "system_ms": 350.3, + "min_ms": 359.7, + "max_ms": 363.1 + }, + { + "name": "oxfmt", + "mean_ms": 278.2, + "stddev_ms": 12.2, + "user_ms": 867.6, + "system_ms": 433.7, + "min_ms": 260.9, + "max_ms": 293 + }, + { + "name": "tsv", + "mean_ms": 65.4, + "stddev_ms": 2.9, + "user_ms": 439.2, + "system_ms": 137.6, + "min_ms": 63.2, + "max_ms": 70.2 + } + ], + "baseline": "tsv", + "speedups": [ + { + "name": "oxfmt", + "ratio": 4.25, + "ratio_stddev": 0.26 + }, + { + "name": "biome", + "ratio": 5.53, + "ratio_stddev": 0.25 + }, + { + "name": "prettier+oxc-parser", + "ratio": 102.99, + "ratio_stddev": 4.59 + }, + { + "name": "prettier", + "ratio": 126.06, + "ratio_stddev": 5.88 + } + ], + "memory": [ + { + "name": "prettier", + "mean_mb": 472.9, + "min_mb": 385.2, + "max_mb": 529.1, + "ratio": 10.01, + "ratio_stddev": 1.38 + }, + { + "name": "prettier+oxc-parser", + "mean_mb": 302.5, + "min_mb": 300.6, + "max_mb": 304.4, + "ratio": 6.4, + "ratio_stddev": 0.37 + }, + { + "name": "biome", + "mean_mb": 211.6, + "min_mb": 207.1, + "max_mb": 213.7, + "ratio": 4.48, + "ratio_stddev": 0.26 + }, + { + "name": "oxfmt", + "mean_mb": 229.5, + "min_mb": 215.2, + "max_mb": 243.2, + "ratio": 4.86, + "ratio_stddev": 0.37 + }, + { + "name": "tsv", + "mean_mb": 47.3, + "min_mb": 44.2, + "max_mb": 50.2 + } + ] + } + ] +} diff --git a/src/routes/docs/benchmarks/benchmarks_formatters.ts b/src/routes/docs/benchmarks/benchmarks_formatters.ts new file mode 100644 index 0000000..0f0fa0d --- /dev/null +++ b/src/routes/docs/benchmarks/benchmarks_formatters.ts @@ -0,0 +1,5 @@ +import type { FormatterBenchmarks } from './formatter_benchmark_data.ts'; + +import json from './benchmarks_formatters.json' with { type: 'json' }; + +export const benchmarks_formatters_json: FormatterBenchmarks = json as FormatterBenchmarks; diff --git a/src/routes/docs/benchmarks/formatter_benchmark_data.ts b/src/routes/docs/benchmarks/formatter_benchmark_data.ts new file mode 100644 index 0000000..ee23057 --- /dev/null +++ b/src/routes/docs/benchmarks/formatter_benchmark_data.ts @@ -0,0 +1,265 @@ +// Types and parser for the formatter comparison benchmark — Prettier, Biome, +// Oxfmt, and tsv on shared corpora. Unlike the tsv bench reports, that harness +// emits no JSON: its numbers exist only as the hyperfine console dump embedded +// in its README between the `BENCHMARK_RESULTS_START`/`END` markers, plus a +// `## Versions` list and a `_Measured on: …_` machine line. This module turns +// that text into data; `benchmarks_formatters.gen.json.ts` writes it out. + +/** A single formatter's timing in one scenario. All durations in milliseconds. */ +export interface FormatterTiming { + name: string; + mean_ms: number; + stddev_ms: number; + min_ms: number; + max_ms: number; + user_ms: number; + system_ms: number; +} + +/** A single formatter's peak-RSS measurement in one scenario. */ +export interface FormatterMemory { + name: string; + mean_mb: number; + min_mb: number; + max_mb: number; + /** Ratio to the scenario's lowest-memory formatter; absent on that baseline row. */ + ratio?: number; + ratio_stddev?: number; +} + +/** How a scenario's fastest formatter compares to one of the others. */ +export interface FormatterSpeedup { + name: string; + ratio: number; + ratio_stddev: number; +} + +/** + * A formatter's parse-check result over the scenario's corpus. The harness runs + * this before timing so a tool that rejects files isn't credited for skipping + * them. + */ +export interface FormatterPreflight { + name: string; + /** Files the formatter refused to parse. */ + rejected: number; + /** The formatter's binary never launched, so its timing row is meaningless. */ + unavailable: boolean; +} + +/** One benchmark scenario — a corpus benched across every formatter that supports it. */ +export interface FormatterScenario { + /** Slug derived from `name`, e.g. `large-single-file`. */ + id: string; + name: string; + /** The corpus, as the harness describes it. */ + target: string; + warmup_runs: number; + benchmark_runs: number; + preflight: Array; + timings: Array; + /** The fastest formatter and its margin over each other one. */ + baseline: string; + speedups: Array; + memory: Array; +} + +/** + * The parsed formatter comparison. Only scenarios tsv participates in are + * included — it has no JSX/TSX parser, so the harness runs it on the JSX-free + * corpora only and the other scenarios have no tsv row to compare against. + */ +export interface FormatterBenchmarks { + /** + * The machine the numbers came from. The ratios move with it — Biome, Oxfmt, + * and tsv scale across cores while Prettier is effectively serial. + */ + machine: string; + /** Formatter name to version string, e.g. `prettier` to `3.9.1`. */ + versions: Record; + scenarios: Array; +} + +const to_slug = (value: string): string => + value + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + +/** Milliseconds from a hyperfine duration and its unit, e.g. `1.597` + `s`. */ +const to_ms = (value: string, unit: string): number => { + const n = Number(value); + if (unit === 's') return n * 1000; + if (unit === 'ms') return n; + if (unit === 'µs' || unit === 'us') return n / 1000; + throw new Error(`unknown duration unit: ${unit}`); +}; + +// The console dump the harness writes into its README. +const RESULTS_START = ''; +const RESULTS_END = ''; + +const SCENARIO_RE = /^=+\nBenchmarking (.+)\n=+$/gm; +const TARGET_RE = /^Target: (.+)$/m; +const RUNS_RE = /^- (\d+) warmup runs?, (\d+) benchmark runs?$/m; +const TIMING_RE = + /^Benchmark \d+: (.+)\n\s*Time \(mean ± σ\):\s*([\d.]+) (\S+) ±\s*([\d.]+) (\S+)\s*\[User: ([\d.]+) (\S+), System: ([\d.]+) (\S+)\]\n\s*Range \(min … max\):\s*([\d.]+) (\S+) …\s*([\d.]+) (\S+)/gm; +const SPEEDUP_BASELINE_RE = /^Summary\n\s*(.+?) ran$/m; +const SPEEDUP_RE = /^\s*([\d.]+) ± ([\d.]+) times faster than (.+)$/gm; +const MEMORY_RE = + /^\s*(\S+): ([\d.]+) MB \(min: ([\d.]+) MB, max: ([\d.]+) MB(?:, ([\d.]+) ± ([\d.]+) times more than \S+)?\)$/gm; +const PREFLIGHT_HEADING = 'Preflight (per-formatter parse check):'; +const PREFLIGHT_RE = /^\s{2}(\S+): (clean|unavailable|\d+ rejected)/gm; +const VERSIONS_RE = /^## Versions\n\n((?:- \*\*.+\*\*: .+\n)+)/m; +const VERSION_RE = /^- \*\*(.+?)\*\*: (.+)$/gm; +const MACHINE_RE = /^_Measured on: (.+?)(?: — |_$)/m; + +/** The text of one section of a scenario block, between two of its headings. */ +const slice_section = (block: string, start: string, ...ends: Array): string => { + const from = block.indexOf(start); + if (from === -1) return ''; + const rest = block.slice(from + start.length); + const to = ends.reduce((lowest, end) => { + const i = rest.indexOf(end); + return i === -1 || i >= lowest ? lowest : i; + }, rest.length); + return rest.slice(0, to); +}; + +const parse_timings = (block: string): Array => + [...block.matchAll(TIMING_RE)].map((m) => ({ + name: m[1]!.trim(), + mean_ms: to_ms(m[2]!, m[3]!), + stddev_ms: to_ms(m[4]!, m[5]!), + user_ms: to_ms(m[6]!, m[7]!), + system_ms: to_ms(m[8]!, m[9]!), + min_ms: to_ms(m[10]!, m[11]!), + max_ms: to_ms(m[12]!, m[13]!) + })); + +const parse_preflight = (block: string): Array => + [...slice_section(block, PREFLIGHT_HEADING, 'Benchmark 1:').matchAll(PREFLIGHT_RE)].map((m) => ({ + name: m[1]!, + rejected: Number.parseInt(m[2]!, 10) || 0, + unavailable: m[2] === 'unavailable' + })); + +const parse_memory = (block: string): Array => + [...slice_section(block, 'Memory Usage:', ' benchmark complete!').matchAll(MEMORY_RE)].map( + (m) => ({ + name: m[1]!, + mean_mb: Number(m[2]), + min_mb: Number(m[3]), + max_mb: Number(m[4]), + ...(m[5] === undefined ? null : { ratio: Number(m[5]), ratio_stddev: Number(m[6]) }) + }) + ); + +const parse_scenario = (name: string, block: string): FormatterScenario => { + const runs = RUNS_RE.exec(block); + const summary = slice_section(block, 'Summary\n', 'Memory Usage:', ' benchmark complete!'); + const timings = parse_timings(block); + // A scenario banner with no timings under it means the timing lines changed + // shape, not that the harness measured nothing — hyperfine always prints them. + if (timings.length === 0) { + throw new Error(`formatter benchmarks: scenario "${name}" has no parseable timings`); + } + // Distinguish "not measured" from "misparsed": these sections are optional (no + // GNU time, no preflight in the JSX scenarios), but a section that's present + // and yields no rows is a broken pattern. + const memory = parse_memory(block); + if (block.includes('Memory Usage:') && memory.length === 0) { + throw new Error(`formatter benchmarks: scenario "${name}" has an unparseable memory section`); + } + const preflight = parse_preflight(block); + if (block.includes(PREFLIGHT_HEADING) && preflight.length === 0) { + throw new Error( + `formatter benchmarks: scenario "${name}" has an unparseable preflight section` + ); + } + return { + id: to_slug(name), + name, + target: TARGET_RE.exec(block)?.[1] ?? '', + warmup_runs: Number(runs?.[1] ?? 0), + benchmark_runs: Number(runs?.[2] ?? 0), + preflight, + timings, + baseline: SPEEDUP_BASELINE_RE.exec(block)?.[1] ?? '', + speedups: [...summary.matchAll(SPEEDUP_RE)].map((m) => ({ + name: m[3]!.trim(), + ratio: Number(m[1]), + ratio_stddev: Number(m[2]) + })), + memory + }; +}; + +/** + * Parse the formatter comparison out of the bench harness's README, keeping only + * the scenarios tsv runs in. + * + * Throws rather than returning a partial result: the harness publishes no JSON, + * so this parses prose, and a drifted heading or timing line would otherwise + * strip scenarios from the site silently. The caller decides what a MISSING + * README means (the sibling checkout is optional); everything past that point is + * a broken contract. + * + * @param readme - the full README text, markers included + * @returns the parsed benchmarks + * @throws if the results markers, the scenario banners, the versions list, the + * machine line, or any tsv scenario's numbers can't be parsed + */ +export const parse_formatter_benchmarks = (readme: string): FormatterBenchmarks => { + const start = readme.indexOf(RESULTS_START); + const end = readme.indexOf(RESULTS_END); + if (start === -1 || end === -1 || end < start) { + throw new Error( + `formatter benchmarks: no ${RESULTS_START} / ${ + RESULTS_END + } block — is this the harness's readme?` + ); + } + const results = readme.slice(start + RESULTS_START.length, end); + + // Split on the scenario banners, pairing each title with the text that follows + // it up to the next banner. + const headings = [...results.matchAll(SCENARIO_RE)]; + if (headings.length === 0) { + throw new Error('formatter benchmarks: results block carries no scenario banners'); + } + const parsed = headings.map(({ 0: heading, 1: name, index }, i) => + parse_scenario(name!.trim(), results.slice(index + heading.length, headings[i + 1]?.index)) + ); + // tsv has no JSX/TSX parser, so it sits out some scenarios by design — but if it + // ran in NONE of them, either the harness stopped benching tsv or the timing + // labels drifted, and the site would render a comparison without its subject. + const scenarios = parsed.filter((scenario) => + scenario.timings.some((timing) => timing.name === 'tsv') + ); + if (scenarios.length === 0) { + throw new Error( + `formatter benchmarks: no scenario includes a tsv row (found ${parsed + .map((s) => s.id) + .join(', ')})` + ); + } + + const versions: Record = {}; + const versions_block = VERSIONS_RE.exec(readme)?.[1] ?? ''; + for (const m of versions_block.matchAll(VERSION_RE)) { + versions[m[1]!.toLowerCase()] = m[2]!.trim(); + } + if (!versions.tsv) { + throw new Error("formatter benchmarks: no tsv version in the readme's `## Versions` list"); + } + + // The ratios move with the core count, so a report that can't say which machine + // produced it isn't publishable. + const machine = MACHINE_RE.exec(readme)?.[1]?.trim(); + if (!machine) { + throw new Error('formatter benchmarks: no `_Measured on: …_` machine line'); + } + + return { machine, versions, scenarios }; +}; diff --git a/src/routes/docs/introduction/+page.svelte b/src/routes/docs/introduction/+page.svelte index dfefa5f..62d8341 100644 --- a/src/routes/docs/introduction/+page.svelte +++ b/src/routes/docs/introduction/+page.svelte @@ -4,9 +4,9 @@ import TomeLink from '@fuzdev/fuz_ui/TomeLink.svelte'; import TomeSection from '@fuzdev/fuz_ui/TomeSection.svelte'; import TomeSectionHeader from '@fuzdev/fuz_ui/TomeSectionHeader.svelte'; - import {tome_get_by_slug} from '@fuzdev/fuz_ui/tome.ts'; + import { tome_get_by_slug } from '@fuzdev/fuz_ui/tome.ts'; import Svg from '@fuzdev/fuz_ui/Svg.svelte'; - import {logo_tsv} from '@fuzdev/fuz_ui/logos.ts'; + import { logo_tsv } from '@fuzdev/fuz_ui/logos.ts'; const LIBRARY_ITEM_NAME = 'introduction'; @@ -47,32 +47,29 @@ reconstruct_locations(ast, 'const x = 1;');`;

Compared to Oxc, Biome, and SWC, tsv is a set of focused tools, not a generic language - platform, so the focus is web standards and there's no support for JSX/SCSS/etc, beyond Svelte - as the only JS framework. The extensibility story is currently limited to using its Rust - crates as libraries; bridging to JS or WASM plugins is an open question, but may not be - supported. + platform, so the focus is Web standards + Svelte and there's no support for JSX/SCSS/etc. The + extensibility story is currently limited to using its Rust crates as libraries (or forking); + bridging to JS or WASM plugins is an open question (leaning against).

tsv prioritizes, in order:

  1. correctness (Svelte and TypeScript conformance, spec adherence for HTML/CSS/JS)
  2. speed
  3. binary size and memory usage
  4. -
  5. extensibility (valued but deprioritized)
  6. +
  7. extensibility (valued but deprioritized), modularity, and reusability

- See the benchmarks for stats. Compared to Oxc - and Biome, tsv is significantly faster, smaller, and uses less memory to parse and format its supported - languages + See the for stats. Compared to Oxc and Biome, tsv is faster, smaller, + and uses less memory to parse and format its supported languages.

- This is an early release, and reports and feedback are appreciated - see the + This is an early release, and reports and feedback are appreciated — see the issues and discussions.

- AI disclosure: this codebase is mostly LLM-generated, and the usual caveats apply. The first - release took 7 months and ~1800 manual commits. It's a high-effort project that prioritizes - quality. + AI disclosure: this codebase is mostly LLM-generated, and the usual caveats apply. It's a + high-effort project that prioritizes quality.

These docs are a work in progress. For design details see the for size and performance details.

- Native builds are not yet available but are coming in v0.2, see + Native builds are not yet published — follow issue 139.

@@ -137,11 +134,11 @@ reconstruct_locations(ast, 'const x = 1;');`; diff --git a/src/routes/docs/playground/+page.svelte b/src/routes/docs/playground/+page.svelte index f9c38ed..aefc746 100644 --- a/src/routes/docs/playground/+page.svelte +++ b/src/routes/docs/playground/+page.svelte @@ -1,16 +1,16 @@ diff --git a/src/routes/docs/playground/Playground.svelte b/src/routes/docs/playground/Playground.svelte index ba20a09..980fba7 100644 --- a/src/routes/docs/playground/Playground.svelte +++ b/src/routes/docs/playground/Playground.svelte @@ -5,13 +5,14 @@ import Code from '@fuzdev/fuz_code/Code.svelte'; import CodeTextarea from '@fuzdev/fuz_code/CodeTextarea.svelte'; - import {supports_css_highlight_api} from '@fuzdev/fuz_code/highlight_manager.ts'; - import {to_error_message} from '@fuzdev/fuz_util/error.ts'; + import { supports_css_highlight_api } from '@fuzdev/fuz_code/highlight_manager.ts'; + import CopyToClipboard from '@fuzdev/fuz_ui/CopyToClipboard.svelte'; + import { to_error_message } from '@fuzdev/fuz_util/error.ts'; - import {playground_example} from './playground_example.ts'; + import { playground_example } from './playground_example.ts'; // `@fuzdev/tsv_wasm` is loaded lazily, in the browser only — a dynamic import - // so the ~900KB WASM lands in its own chunk, fetched the first time this + // so the ~900KB-gzipped WASM lands in its own chunk, fetched the first time this // component mounts and never pulled into `/docs` or the prerendered HTML. let tsv: typeof import('@fuzdev/tsv_wasm') | null = $state(null); let load_error: string | null = $state(null); @@ -23,7 +24,7 @@ const ready = $derived(tsv !== null); // A tsv call's outcome: its string result, or the thrown error's message. - type Outcome = {value: string; error: null} | {value: null; error: string}; + type Outcome = { value: string; error: null } | { value: null; error: string }; // Run a tsv call, capturing a thrown error as a message; `null` until the WASM // loads. Lets `formatted` and `ast` share one shape and recompute as `source` changes @@ -31,9 +32,9 @@ const run = (fn: (t: NonNullable) => string): Outcome | null => { if (!tsv) return null; try { - return {value: fn(tsv), error: null}; + return { value: fn(tsv), error: null }; } catch (err) { - return {value: null, error: to_error_message(err)}; + return { value: null, error: to_error_message(err) }; } }; @@ -72,6 +73,8 @@ }; }); + const is_base = $derived(source === playground_example); + const reset = (): void => { source = playground_example; }; @@ -84,8 +87,8 @@ }; -
- +
+ @@ -112,7 +115,10 @@

formatted

AST

- +
+ + +
{/if} @@ -132,6 +138,24 @@ section :global(.ast) { max-height: 500px; } + /* float a copy button over the AST pane's top-right corner; it stays pinned as + the pane scrolls since it's absolute to this wrapper, not inside the scroller */ + .ast_output { + position: relative; + } + .ast_output :global(.ast_copy) { + --font_size: var(--font_size_lg); + position: absolute; + top: var(--space_xs); + /* clear the pane's scrollbar */ + right: var(--space_md); + z-index: 1; + background: var(--shade_00); + } + /* TODO hacky */ + .ast_output :global(.ast_copy > div) { + width: auto !important; + } .error { color: var(--color_e_40); white-space: pre-wrap; diff --git a/src/routes/docs/tomes.ts b/src/routes/docs/tomes.ts index 733ca43..c53039a 100644 --- a/src/routes/docs/tomes.ts +++ b/src/routes/docs/tomes.ts @@ -1,4 +1,4 @@ -import type {Tome} from '@fuzdev/fuz_ui/tome.ts'; +import type { Tome } from '@fuzdev/fuz_ui/tome.ts'; import IntroductionPage from './introduction/+page.svelte'; import PlaygroundPage from './playground/+page.svelte'; import BenchmarksPage from './benchmarks/+page.svelte'; @@ -10,7 +10,7 @@ export const tomes: Array = [ Component: IntroductionPage, related_tomes: ['playground', 'benchmarks'], related_modules: [], - related_declarations: [], + related_declarations: [] }, { slug: 'playground', @@ -18,7 +18,7 @@ export const tomes: Array = [ Component: PlaygroundPage, related_tomes: ['introduction', 'benchmarks'], related_modules: [], - related_declarations: [], + related_declarations: [] }, { slug: 'benchmarks', @@ -26,6 +26,6 @@ export const tomes: Array = [ Component: BenchmarksPage, related_tomes: ['introduction', 'playground'], related_modules: [], - related_declarations: [], - }, + related_declarations: [] + } ]; diff --git a/src/routes/example.test.ts b/src/routes/example.test.ts index 57684dd..7e61731 100644 --- a/src/routes/example.test.ts +++ b/src/routes/example.test.ts @@ -1,4 +1,4 @@ -import {assert, test} from 'vitest'; +import { assert, test } from 'vitest'; test('tests work', () => { assert.strictEqual('ok', 'OK'.toLowerCase()); diff --git a/src/routes/library.ts b/src/routes/library.ts index b2aa8df..b47f208 100644 --- a/src/routes/library.ts +++ b/src/routes/library.ts @@ -1,5 +1,5 @@ -import {library_json_from_modules} from '@fuzdev/fuz_util/library_json.ts'; -import {modules} from 'virtual:svelte-docinfo'; +import { library_json_from_modules } from '@fuzdev/fuz_util/library_json.ts'; +import { modules } from 'virtual:svelte-docinfo'; import pkg_json from 'virtual:pkg.json'; export const library_json = library_json_from_modules(pkg_json, modules); diff --git a/src/test/benchmark_data.test.ts b/src/test/benchmark_data.test.ts index 0f6e8d0..8f3c92a 100644 --- a/src/test/benchmark_data.test.ts +++ b/src/test/benchmark_data.test.ts @@ -1,9 +1,34 @@ -import {assert, describe, test} from 'vitest'; +import { assert, describe, test } from 'vitest'; -import {benchmarks_json} from '$routes/docs/benchmarks/benchmarks.ts'; +import { benchmarks_json } from '$routes/docs/benchmarks/benchmarks.ts'; +import { benchmarks_conformance_json } from '$routes/docs/benchmarks/benchmarks_conformance.ts'; +import { benchmarks_cross_runtime_json } from '$routes/docs/benchmarks/benchmarks_cross_runtime.ts'; +import { benchmarks_formatters_json } from '$routes/docs/benchmarks/benchmarks_formatters.ts'; import { + benchmarks_cli, + CLI_LABELS, + CLI_SCENARIO_KEYS, + cli_memory_ratio_range, + cli_speedup_vs_tsv, + CLI_TS_REPO_KEY +} from '$routes/docs/benchmarks/benchmarks_cli.ts'; +import { + benchmark_speedup, + categorize_size_capability, + compute_baseline_ratio, derive_benchmark_groups, + derive_conformance_groups, + derive_corpus_repos, + derive_cross_runtime_groups, + derive_size_groups, derive_speedup_summary, + format_coverage_percent, + format_ratio_approx, + format_ratio_range, + format_speedup_signed, + order_cross_runtime_runtimes, + OXC_FULL_LABEL, + OXFMT_WASM_LABEL } from '$routes/docs/benchmarks/benchmark_data.ts'; // Shape gate for the committed benchmarks.json: the bench report format drifts @@ -12,17 +37,18 @@ import { // When `npm run update-benchmarks` pulls in a new shape, these fail loudly. describe('benchmarks.json shape', () => { test('baseline version is current', () => { - assert.isAtLeast(benchmarks_json.version, 4); + // version 5 is the first per-runtime report shape (carries `runtime`) + assert.isAtLeast(benchmarks_json.version, 5); }); - test('binary sizes include the ratio anchors', () => { + test('binary sizes include the flagship tsv builds', () => { const labels = benchmarks_json.binary_sizes.map((s) => s.label); - assert.include(labels, 'tsv (native)'); // native anchor - assert.include(labels, 'tsv_wasm'); // wasm anchor (the full build) + assert.include(labels, 'tsv (napi)'); // flagship N-API build (perf report anchor) + assert.include(labels, 'tsv_wasm'); // the full wasm build — smallest full-toolchain, size baseline }); test('versions carries the keys the meta component renders', () => { - const {versions} = benchmarks_json; + const { versions } = benchmarks_json; assert.isString(versions.svelte); assert.isString(versions.acorn_ts); assert.isString(versions.prettier); @@ -35,13 +61,151 @@ describe('benchmarks.json shape', () => { } }); - test('every group derives a canonical entry and a timed-set count', () => { + test('every group leads with its canonical entry (the default anchor) and a timed-set count', () => { const groups = derive_benchmark_groups(benchmarks_json); assert.isAtLeast(groups.length, 6); // format+parse × svelte/typescript/css for (const group of groups) { const key = `${group.operation}/${group.language}`; assert.ok(group.canonical_entry, `${key} has no canonical entry`); + // the canonical reference sorts first, so it's the default 1.0x anchor the + // shared component reads off the leading row + assert.strictEqual(group.entries[0]?.category, 'canonical', `${key} canonical leads`); assert.isNotNull(group.files_iterated, `${key} has no files_iterated`); + for (const entry of group.entries) { + if (entry.disabled) continue; + // measured entries render the whole-sweep mean (total corpus time); a + // zero would misread as an instantaneous run + assert.isAbove(entry.mean_ns, 0, `${key}/${entry.name} has no mean`); + } + } + }); + + test('svelte/css parse groups get disabled oxc placeholders, typescript keeps real ones', () => { + const groups = derive_benchmark_groups(benchmarks_json); + const parse = (language: string) => + groups.find((g) => g.operation === 'parse' && g.language === language); + + // typescript actually runs oxc-parser — its oxc entries are real, not placeholders + const ts = parse('typescript'); + const ts_oxc = ts?.entries.filter((e) => e.category === 'oxc') ?? []; + assert.isNotEmpty(ts_oxc); + for (const e of ts_oxc) assert.isNotOk(e.disabled, `${e.name} should be a real entry`); + + // svelte and css mirror those oxc entries in, disabled, in the same fixed slot: + // directly after the biome placeholder (both lead the cross-tool comparisons, + // right after the canonical row) and before tsv's json wires + for (const language of ['svelte', 'css']) { + const group = parse(language); + assert.ok(group, `${language} parse group missing`); + const oxc = group.entries.filter((e) => e.category === 'oxc'); + assert.strictEqual(oxc.length, ts_oxc.length, `${language} oxc placeholder count`); + for (const e of oxc) { + assert.ok(e.disabled, `${language} ${e.name} should be disabled`); + assert.strictEqual(e.bar_fraction, 0, `${language} ${e.name} bar`); + } + const names = group.entries.map((e) => e.name); + const first_biome = names.findIndex((n) => n.includes('biome')); + const first_oxc = names.findIndex((n) => n.includes('oxc')); + const first_json = names.findIndex((n) => n.endsWith('-json') || n.endsWith('-no-locations')); + assert.strictEqual(first_oxc, first_biome + 1, `${language} oxc directly after biome`); + assert.isBelow(first_oxc, first_json, `${language} oxc before the tsv json entries`); + } + }); + + test('every parse group gets a disabled biome placeholder, since biome never exposes a parser to JS', () => { + const groups = derive_benchmark_groups(benchmarks_json); + for (const language of ['svelte', 'typescript', 'css']) { + const group = groups.find((g) => g.operation === 'parse' && g.language === language); + assert.ok(group, `${language} parse group missing`); + const biome = group.entries.filter((e) => e.category === 'biome'); + assert.strictEqual(biome.length, 1, `${language} biome placeholder count`); + const [entry] = biome; + assert.ok(entry, `${language} biome entry missing`); + assert.ok(entry.disabled, `${language} biome entry should be disabled`); + assert.strictEqual(entry.bar_fraction, 0, `${language} biome bar`); + + // biome leads the cross-tool comparisons: directly after the single canonical + // row (index 0), before tsv's json wires + const names = group.entries.map((e) => e.name); + const first_biome = names.findIndex((n) => n.includes('biome')); + const first_json = names.findIndex((n) => n.endsWith('-json') || n.endsWith('-no-locations')); + assert.strictEqual(first_biome, 1, `${language} biome directly after the canonical row`); + assert.isBelow(first_biome, first_json, `${language} biome before the tsv json entries`); + } + }); + + test('svelte/css format groups get disabled dprint placeholders, typescript keeps real ones', () => { + const groups = derive_benchmark_groups(benchmarks_json); + const format = (language: string) => + groups.find((g) => g.operation === 'format' && g.language === language); + + // typescript actually runs dprint — its entry is real, not a placeholder + const ts = format('typescript'); + const ts_dprint = ts?.entries.filter((e) => e.category === 'dprint') ?? []; + + // The dprint row postdates older reports. With no measured entry there is + // nothing to mirror, and the derivation deliberately leaves every group + // untouched — so assert exactly that (no invented rows) and stop. Refreshing + // the report is what promotes this to the full assertion below. + if (ts_dprint.length === 0) { + for (const language of ['svelte', 'css']) { + const group = format(language); + assert.isEmpty( + group?.entries.filter((e) => e.category === 'dprint') ?? [], + `${language} must not invent a dprint row when the report carries none` + ); + } + return; + } + + for (const e of ts_dprint) assert.isNotOk(e.disabled, `${e.name} should be a real entry`); + + // svelte and css mirror it in, disabled: dprint's `@dprint/typescript` plugin + // rejects CSS and Svelte outright, so those groups have no real entry + for (const language of ['svelte', 'css']) { + const group = format(language); + assert.ok(group, `${language} format group missing`); + const dprint = group.entries.filter((e) => e.category === 'dprint'); + assert.strictEqual(dprint.length, ts_dprint.length, `${language} dprint placeholder count`); + for (const e of dprint) { + assert.ok(e.disabled, `${language} ${e.name} should be disabled`); + assert.strictEqual(e.bar_fraction, 0, `${language} ${e.name} bar`); + } + // dprint sits between biome and oxc in the shared cross-tool ordering + const names = group.entries.map((e) => e.name); + const first_biome = names.findIndex((n) => n.includes('biome')); + const first_dprint = names.findIndex((n) => n.includes('dprint')); + assert.strictEqual(first_dprint, first_biome + 1, `${language} dprint directly after biome`); + } + }); + + test('format/parse rows follow the fixed canonical → biome → dprint → oxc → json → internal order', () => { + // independently mirrors the requested row order so a regression in the + // derivation's comparator fails here + const tier = (name: string, category: string): number => { + if (category === 'canonical') return 0; + if (category === 'biome') return 1; + if (category === 'dprint') return 2; + if (category === 'oxc') return 3; + if (name.endsWith('-no-locations')) return 4; // tsv json, span-only wire + if (name.endsWith('-json')) return 5; // tsv json, loc-carrying wire + return 6; // tsv internal engine + }; + for (const group of derive_benchmark_groups(benchmarks_json)) { + const key = `${group.operation}/${group.language}`; + const tiers = group.entries.map((e) => tier(e.name, e.category)); + assert.deepEqual( + tiers, + [...tiers].toSorted((a, b) => a - b), + `${key} tier order` + ); + // wasm precedes native within a tier + for (let i = 1; i < group.entries.length; i++) { + if (tiers[i] !== tiers[i - 1]) continue; + const prev_wasm = group.entries[i - 1]!.name.includes('wasm'); + const curr_wasm = group.entries[i]!.name.includes('wasm'); + assert.isFalse(!prev_wasm && curr_wasm, `${key} native precedes wasm within a tier`); + } } }); @@ -54,4 +218,377 @@ describe('benchmarks.json shape', () => { assert.isDefined(row.format_css, row.variant); } }); + + test('binary sizes group by capability with one smallest-build ratio anchor', () => { + const groups = derive_size_groups(benchmarks_json.binary_sizes); + // full / formatter / parser, in that order, all present in the current data + assert.deepStrictEqual( + groups.map((g) => g.capability), + ['full', 'formatter', 'parser'] + ); + for (const group of groups) { + assert.isNotEmpty(group.entries); + // every entry lands in the group its capability names + for (const e of group.entries) { + assert.strictEqual(categorize_size_capability(e.label), group.capability); + } + // the group's default ratio anchor (its 1.0x row) is the single smallest real + // build, and it leads the group — the shared component reads every ratio off + // the first row. Disabled placeholders (e.g. oxfmt's absent wasm build) never + // sort first. + const real = group.entries.filter((e) => !e.disabled); + const smallest = real.reduce((a, b) => (a.bytes <= b.bytes ? a : b)); + assert.strictEqual( + group.entries[0]?.label, + smallest.label, + `${group.capability} smallest leads` + ); + } + // the parser group pits tsv against oxc-parser in both kinds + const parser = groups.find((g) => g.capability === 'parser'); + const parser_labels = parser?.entries.map((e) => e.label) ?? []; + assert.include(parser_labels, 'oxc-parser (wasm)'); + assert.include(parser_labels, 'oxc-parser (napi)'); + }); + + test('full toolchain group carries the combined oxc-parser + oxfmt entry', () => { + const sizes = benchmarks_json.binary_sizes; + const groups = derive_size_groups(sizes); + const full = groups.find((g) => g.capability === 'full'); + const combined = full?.entries.find((e) => e.label === OXC_FULL_LABEL); + assert.ok(combined, 'combined oxc entry missing from full toolchain group'); + + // its bytes and gzip are the sum of oxc's separate parser and formatter builds + const oxc_parser = sizes.find((s) => s.label === 'oxc-parser (napi)'); + const oxfmt = sizes.find((s) => s.label === 'oxfmt (napi)'); + assert.ok(oxc_parser && oxfmt, 'source oxc builds missing'); + assert.strictEqual(combined.bytes, oxc_parser.bytes + oxfmt.bytes); + assert.strictEqual(combined.gzip_bytes, oxc_parser.gzip_bytes! + oxfmt.gzip_bytes!); + + // native build, colored as oxc + assert.strictEqual(combined.kind, 'native'); + assert.strictEqual(combined.category, 'oxc'); + }); + + test('formatter group gets a disabled oxfmt (wasm) placeholder just above oxfmt (napi), since oxfmt has no wasm build', () => { + const groups = derive_size_groups(benchmarks_json.binary_sizes); + const formatter = groups.find((g) => g.capability === 'formatter'); + assert.ok(formatter, 'formatter group missing'); + + const placeholder = formatter.entries.find((e) => e.label === OXFMT_WASM_LABEL); + assert.ok(placeholder, 'oxfmt (wasm) placeholder missing'); + assert.ok(placeholder.disabled, 'oxfmt (wasm) should be disabled'); + assert.strictEqual(placeholder.kind, 'wasm'); + assert.strictEqual(placeholder.category, 'oxc'); + assert.strictEqual(placeholder.bar_fraction, 0); + + const labels = formatter.entries.map((e) => e.label); + const placeholder_index = labels.indexOf(OXFMT_WASM_LABEL); + const native_index = labels.indexOf('oxfmt (napi)'); + assert.isAbove(native_index, -1, 'oxfmt (napi) missing from formatter group'); + assert.strictEqual( + placeholder_index, + native_index - 1, + 'placeholder should sit just above oxfmt (napi)' + ); + }); + + test('flagship report is the node runtime', () => { + // the headline detailed view switched to N-API under Node; guards against an + // `update-benchmarks` that pulls the wrong runtime's sibling report + assert.strictEqual(benchmarks_json.runtime, 'node'); + }); +}); + +// Shape gate for the committed conformance report `benchmarks_conformance.json` +// (tsv's `report.conformance.node.json` — the parse-coverage surface over the +// deliberately-hard fixture suites, disjoint from the perf corpus, Svelte set minus +// canonical-rejects), consumed by the Parse conformance section. +describe('benchmarks_conformance.json shape', () => { + test('report is the conformance surface at the current version', () => { + assert.isAtLeast(benchmarks_conformance_json.version, 6); + assert.strictEqual(benchmarks_conformance_json.corpus_kind, 'conformance'); + assert.strictEqual(benchmarks_conformance_json.runtime, 'node'); + }); + + test('conformance report is parse-only', () => { + for (const entry of benchmarks_conformance_json.entries) { + assert.match(entry.group, /^parse\//, `${entry.group}/${entry.name}`); + } + }); + + test('corpus sources disclose the composition', () => { + assert.isNotEmpty(benchmarks_conformance_json.corpus_sources ?? []); + }); + + test('derives one coverage group per language, each with a tsv row and full coverage data', () => { + const groups = derive_conformance_groups(benchmarks_conformance_json); + assert.strictEqual(groups.length, 3); // svelte / typescript / css + assert.deepEqual( + groups.map((g) => g.language), + ['svelte', 'typescript', 'css'] + ); + for (const group of groups) { + assert.ok( + group.rows.some((r) => r.name === 'tsv'), + `${group.language} has a tsv row` + ); + // rows are ordered by coverage, highest first + const fractions = group.rows.map((r) => r.coverage_fraction); + assert.deepEqual( + fractions, + [...fractions].toSorted((a, b) => b - a), + `${group.language} rows descend by coverage` + ); + for (const row of group.rows) { + assert.isAbove(row.files_total, 0, `${group.language}/${row.name} total`); + assert.isAtLeast(row.coverage_fraction, 0); + assert.isAtMost(row.coverage_fraction, 1); + // engine-level rows only — binding/materialization variants are folded + assert.notMatch(row.name, /-internal|wasm-|-wasm/, `${group.language}/${row.name}`); + } + } + }); + + test('coverage percent floors — only exact totality reads 100%', () => { + // 44219/44220 rounds to 100.00% but must not display as it: floor, so a + // visibly non-total count never sits beside a "100.00%" label. + assert.strictEqual(format_coverage_percent(44_219 / 44_220), '99.99%'); + assert.strictEqual(format_coverage_percent(1), '100.00%'); + assert.strictEqual(format_coverage_percent(0.998549), '99.85%'); + assert.strictEqual(format_coverage_percent(0), '0.00%'); + }); +}); + +describe('derive_corpus_repos', () => { + test('maps the committed corpus sources to deduped org/name repo links', () => { + const repos = derive_corpus_repos(benchmarks_json.corpus_sources); + assert.isNotEmpty(repos); + // one entry per URL — no repo appears twice even though svelte.dev contributes + // several source subpaths + const urls = repos.map((r) => r.url); + assert.strictEqual(new Set(urls).size, urls.length, 'urls are distinct'); + const svelte_dev = repos.filter((r) => r.url === 'https://github.com/sveltejs/svelte.dev'); + assert.strictEqual(svelte_dev.length, 1, 'svelte.dev collapses to one entry'); + // each label is the linkified `org/name`, derived from (and ending) its URL + for (const repo of repos) { + assert.match(repo.label, /^[^/]+\/[^/]+$/, repo.url); + assert.isTrue(repo.url.endsWith(repo.label), `${repo.url} ends with ${repo.label}`); + } + }); + + test('collapses shared repos and drops sources with no mapped repo', () => { + const repos = derive_corpus_repos([ + { path: '../zzz/src', files: 1 }, + { path: '../svelte.dev/apps/svelte.dev/src', files: 1 }, + { path: '../svelte.dev/packages/repl/src', files: 1 }, // same repo → collapsed + { path: 'benches/js/.cache/svelte_styles', files: 1 } // not a repo → dropped + ]); + assert.deepStrictEqual(repos, [ + { url: 'https://github.com/fuzdev/zzz', label: 'fuzdev/zzz' }, + { + url: 'https://github.com/sveltejs/svelte.dev', + label: 'sveltejs/svelte.dev' + } + ]); + }); + + test('handles a missing corpus_sources field', () => { + assert.deepStrictEqual(derive_corpus_repos(undefined), []); + }); +}); + +describe('format_speedup_signed', () => { + test('anchor-and-faster entries read as a plain multiple', () => { + assert.strictEqual(format_speedup_signed(1), '1.00x'); + assert.strictEqual(format_speedup_signed(2.5), '2.50x'); + assert.strictEqual(format_speedup_signed(12.3), '12.3x'); // >= 10 drops to one decimal + }); + + test('slower entries negate the reciprocal so the factor is directly legible', () => { + assert.strictEqual(format_speedup_signed(0.15), '-6.67x'); + assert.strictEqual(format_speedup_signed(0.05), '-20.0x'); // >= 10 magnitude → one decimal + assert.strictEqual(format_speedup_signed(0.98), '-1.02x'); // near-parity sign flip + }); +}); + +describe('compute_baseline_ratio', () => { + test('speed reads the anchor as the reference — faster entries exceed 1', () => { + // anchor 100ns; a 50ns entry is 2x faster, a 200ns entry is half the speed + assert.strictEqual(compute_baseline_ratio('speed', 50, 100), 2); + assert.strictEqual(compute_baseline_ratio('speed', 200, 100), 0.5); + }); + + test('size reads the anchor as the reference — bigger entries exceed 1', () => { + // anchor 100 bytes; a 300-byte build is 3x bigger, a 50-byte build is half + assert.strictEqual(compute_baseline_ratio('size', 300, 100), 3); + assert.strictEqual(compute_baseline_ratio('size', 50, 100), 0.5); + }); +}); + +describe('order_cross_runtime_runtimes', () => { + test('reorders the report storage order to node-first display order', () => { + assert.deepStrictEqual(order_cross_runtime_runtimes(['deno', 'node', 'bun']), [ + 'node', + 'deno', + 'bun' + ]); + }); + + test('anchors on the next runtime in display order when node is absent', () => { + assert.deepStrictEqual(order_cross_runtime_runtimes(['bun', 'deno']), ['deno', 'bun']); + assert.deepStrictEqual(order_cross_runtime_runtimes([]), []); + }); +}); + +// Shape gate for the committed cross-runtime `benchmarks_cross_runtime.json` (the +// bench composer's combined `report.json`) — a different, slimmer shape than the +// per-runtime baseline, consumed by the Cross-runtime section. +describe('benchmarks_cross_runtime.json shape', () => { + test('combined report carries the current version and kind', () => { + assert.isAtLeast(benchmarks_cross_runtime_json.version, 5); + assert.strictEqual(benchmarks_cross_runtime_json.kind, 'combined'); + // the committed fixture must be same-vintage — if this trips, re-run every + // runtime and recompose rather than committing a mixed set (the site would + // show the unreliable-ratios warning banner) + assert.notStrictEqual(benchmarks_cross_runtime_json.mixed_vintage, true); + }); + + test('runtimes include the flagship and its cross-runtime peers', () => { + const { runtimes } = benchmarks_cross_runtime_json; + assert.include(runtimes, 'node'); // the flagship the headline view leads with + assert.include(runtimes, 'deno'); + assert.include(runtimes, 'bun'); + }); + + test('every group derives rows with the flagship runtime populated', () => { + const groups = derive_cross_runtime_groups(benchmarks_cross_runtime_json); + assert.isAtLeast(groups.length, 6); // format+parse × svelte/typescript/css + for (const group of groups) { + assert.isAbove(group.rows.length, 0, group.group); + for (const row of group.rows) { + assert.isNumber(row.ops_per_second.node, `${group.group}/${row.name} missing node ops`); + // ratios anchor on node (the display-order base, not the report's + // deno-first storage order), so node's own ratio is exactly 1 + assert.strictEqual(row.ratio_vs_base.node, 1, `${group.group}/${row.name} node anchor`); + } + } + }); +}); + +// Shape gate for the CLI report `benchmarks_cli.ts` derives from the generated +// `benchmarks_formatters.json`. The generator parses prose, so a drifted heading +// or scenario id in the harness's README would silently drop a scenario or its +// `tsv` reference row and render an empty table rather than fail to typecheck. +describe('benchmarks_cli shape', () => { + test('every scenario the page has prose for resolved to generated data', () => { + // a copy entry with no matching scenario id drops silently from the table + assert.deepEqual( + benchmarks_cli.scenarios.map((s) => s.key), + CLI_SCENARIO_KEYS + ); + }); + + test('every scenario carries a tsv reference row with positive metrics', () => { + assert.isAtLeast(benchmarks_cli.scenarios.length, 1); + for (const scenario of benchmarks_cli.scenarios) { + // BenchmarksCli anchors every ratio on the tsv row; without it the table is empty + assert.ok( + scenario.results.some((r) => r.label === 'tsv'), + `${scenario.key} is missing its tsv row` + ); + for (const r of scenario.results) { + assert.isAbove(r.wall_ms, 0, `${scenario.key}/${r.label} wall_ms`); + assert.isAbove(r.cpu_ms, 0, `${scenario.key}/${r.label} cpu_ms`); + // null is legal (a harness run without GNU time measures no memory), 0 is not + assert.ok(r.memory_mb === null || r.memory_mb > 0, `${scenario.key}/${r.label} memory_mb`); + } + } + }); + + test('the derived wall-clock ratios agree with hyperfine own summary', () => { + // The generated report carries the harness's own `Summary` ratios beside the + // raw timings. Recomputing them from the timings and comparing catches a + // misparse that would otherwise render plausible-but-wrong numbers. + for (const scenario of benchmarks_formatters_json.scenarios) { + assert.strictEqual(scenario.baseline, 'tsv', `${scenario.id} baseline`); + for (const speedup of scenario.speedups) { + const derived = cli_speedup_vs_tsv( + scenario.id, + CLI_LABELS[speedup.name] ?? speedup.name, + 'wall_ms' + ); + assert.isDefined(derived, `${scenario.id}/${speedup.name}`); + // hyperfine derives its summary from full-precision means but prints the + // timings rounded, so recomputing from the printed numbers lands within a + // fraction of a percent — wide enough for that, far too tight to hide a + // misparse (a wrong unit would be off by 1000x) + assert.closeTo( + derived, + speedup.ratio, + speedup.ratio * 0.01, + `${scenario.id}/${speedup.name}` + ); + } + } + }); + + test('every formatter accepts the whole corpus, as the page claims', () => { + // The page's notes say the preflight parse check found nothing rejected, so + // no formatter is credited for skipping files. That's a claim about the data. + for (const scenario of benchmarks_formatters_json.scenarios) { + assert.isNotEmpty(scenario.preflight, `${scenario.id} ran no preflight`); + for (const entry of scenario.preflight) { + assert.strictEqual(entry.rejected, 0, `${scenario.id}/${entry.name} rejected files`); + assert.isFalse(entry.unavailable, `${scenario.id}/${entry.name} never launched`); + } + } + }); +}); + +// The page's prose quotes ratios computed from the reports rather than +// hand-written numbers, so a renamed entry or a dropped scenario would render +// `—` mid-sentence instead of failing. These gate every pair the copy names. +describe('prose ratios resolve', () => { + const IN_PROCESS_PAIRS: Array<[string, string, string]> = [ + ['format/typescript', 'oxfmt', 'tsv'], + ['format/typescript', 'prettier', 'tsv'], + ['format/typescript', 'biome-wasm', 'tsv_wasm'], + ['format/svelte', 'prettier', 'tsv'], + ['format/svelte', 'biome-wasm', 'tsv_wasm'], + ['format/css', 'oxfmt', 'tsv'], + ['format/css', 'biome-wasm', 'tsv_wasm'], + ['parse/typescript', 'oxc-parser', 'tsv-json-no-locations'] + ]; + + test('every in-process pair the TLDR quotes is present and favors tsv', () => { + for (const [group, slower, faster] of IN_PROCESS_PAIRS) { + const ratio = benchmark_speedup(benchmarks_json, group, slower, faster); + assert.isDefined(ratio, `${group}: ${slower} vs ${faster}`); + assert.isAbove(ratio, 1, `${group}: ${slower} vs ${faster}`); + } + }); + + test('every CLI ratio the prose quotes is present', () => { + for (const [label, metric] of [ + ['oxfmt', 'wall_ms'], + ['oxfmt', 'cpu_ms'], + ['biome', 'wall_ms'] + ] as const) { + assert.isDefined( + cli_speedup_vs_tsv(CLI_TS_REPO_KEY, label, metric), + `${CLI_TS_REPO_KEY}: ${label} ${metric}` + ); + } + assert.isDefined(cli_memory_ratio_range(CLI_TS_REPO_KEY)); + assert.isDefined(cli_memory_ratio_range()); + }); + + test('approximate formatting drops digits as the ratio grows', () => { + assert.strictEqual(format_ratio_approx(1.66), '1.7x'); + assert.strictEqual(format_ratio_approx(26.241), '26x'); + assert.strictEqual(format_ratio_approx(undefined), '—'); + // floored at both ends so the claim never overstates either bound + assert.strictEqual(format_ratio_range(3.001, 9.89), '3–9x'); + }); }); diff --git a/src/test/formatter_benchmark_data.test.ts b/src/test/formatter_benchmark_data.test.ts new file mode 100644 index 0000000..693daa1 --- /dev/null +++ b/src/test/formatter_benchmark_data.test.ts @@ -0,0 +1,164 @@ +import { assert, describe, test } from 'vitest'; + +import { parse_formatter_benchmarks } from '$routes/docs/benchmarks/formatter_benchmark_data.ts'; + +// A trimmed stand-in for the bench harness's README — one scenario tsv runs in, +// one it sits out, plus the versions and machine lines below the results block. +const readme = `# Benchmark + + + +\`\`\` +========================================= +Benchmarking Large Single File +========================================= + +Target: TypeScript compiler parser.ts (~540KB) +- 2 warmup runs, 5 benchmark runs +- Copy original before each run + + +Preflight (per-formatter parse check): + biome: clean + oxfmt: 3 rejected + tsv: unavailable (command failed to launch) +Benchmark 1: biome + Time (mean ± σ): 1.597 s ± 0.015 s [User: 1.583 s, System: 0.846 s] + Range (min … max): 1.581 s … 1.614 s 5 runs + +Benchmark 2: tsv + Time (mean ± σ): 28.8 ms ± 0.7 ms [User: 15.2 ms, System: 13.5 ms] + Range (min … max): 27.8 ms … 29.5 ms 5 runs + +Summary + tsv ran + 55.41 ± 1.36 times faster than biome + +Memory Usage: + biome: 303.2 MB (min: 291.5 MB, max: 319.4 MB, 13.12 ± 0.62 times more than tsv) + tsv: 23.1 MB (min: 22.5 MB, max: 23.9 MB) + +Large single file benchmark complete! + + +========================================= +Benchmarking Mixed (embedded) +========================================= + +Target: Storybook repository (mixed with embedded languages) +- 1 warmup runs, 3 benchmark runs +- Git reset before each run + +Benchmark 1: oxfmt + Time (mean ± σ): 7.832 s ± 0.209 s [User: 82.370 s, System: 5.801 s] + Range (min … max): 7.622 s … 8.040 s 3 runs + +Mixed (embedded) benchmark complete! +\`\`\` + + + +## Versions + +- **Prettier**: 3.9.1 +- **Biome**: 2.5.1 +- **tsv**: 0.2.0 + +_Measured on: Some CPU · 12 threads · linux x64 — the ratios below depend on the core count._ +`; + +describe('parse_formatter_benchmarks', () => { + test('keeps only the scenarios tsv runs in', () => { + const parsed = parse_formatter_benchmarks(readme); + assert.deepEqual( + parsed.scenarios.map((s) => s.id), + ['large-single-file'] + ); + }); + + test('parses the machine and versions from below the results block', () => { + const parsed = parse_formatter_benchmarks(readme); + assert.equal(parsed.machine, 'Some CPU · 12 threads · linux x64'); + assert.deepEqual(parsed.versions, { prettier: '3.9.1', biome: '2.5.1', tsv: '0.2.0' }); + }); + + test('normalizes timings to milliseconds across units', () => { + const parsed = parse_formatter_benchmarks(readme); + const [biome, tsv] = parsed.scenarios[0]!.timings; + assert.deepEqual(biome, { + name: 'biome', + mean_ms: 1597, + stddev_ms: 15, + user_ms: 1583, + system_ms: 846, + min_ms: 1581, + max_ms: 1614 + }); + assert.equal(tsv!.mean_ms, 28.8); + }); + + test('parses the scenario header, preflight, speedups, and memory', () => { + const scenario = parse_formatter_benchmarks(readme).scenarios[0]!; + assert.equal(scenario.name, 'Large Single File'); + assert.equal(scenario.target, 'TypeScript compiler parser.ts (~540KB)'); + assert.equal(scenario.warmup_runs, 2); + assert.equal(scenario.benchmark_runs, 5); + assert.deepEqual(scenario.preflight, [ + { name: 'biome', rejected: 0, unavailable: false }, + { name: 'oxfmt', rejected: 3, unavailable: false }, + { name: 'tsv', rejected: 0, unavailable: true } + ]); + assert.equal(scenario.baseline, 'tsv'); + assert.deepEqual(scenario.speedups, [{ name: 'biome', ratio: 55.41, ratio_stddev: 1.36 }]); + // the lowest-memory formatter is the ratio baseline, so it carries no ratio + assert.deepEqual(scenario.memory, [ + { + name: 'biome', + mean_mb: 303.2, + min_mb: 291.5, + max_mb: 319.4, + ratio: 13.12, + ratio_stddev: 0.62 + }, + { name: 'tsv', mean_mb: 23.1, min_mb: 22.5, max_mb: 23.9 } + ]); + }); + + // A README that's present but drifted must fail the gen task rather than quietly + // publishing stale or scenario-stripped numbers — only a MISSING readme is benign, + // and that's the caller's call, not the parser's. + test('throws when the results markers are missing', () => { + assert.throws( + () => parse_formatter_benchmarks('# Benchmark\n\nno results here\n'), + /no