Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions .agents/docs/benchmark.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# Benchmark Design

[VOUCHED @hyfdev 2026-08-16]

## Purpose and comparison boundary

The public benchmark measures TaffyJS as a JavaScript product in Node.js. Every timed operation starts at a public JavaScript API and includes the complete implementation cost beneath that call. JavaScript validation and conversion, wrapper and compatibility work, Node-API, emnapi, WASI, Wasm memory transfer, callbacks, output construction, redundant computation, and unnecessary data movement all remain part of the result. The benchmark must not bypass a public wrapper, call a private binding, substitute an internal Rust or C++ timer, preconvert data into a private representation, or subtract work in order to make implementations look more alike.

The benchmark has two comparison groups:

- The Taffy API group runs the same public benchmark source against `@taffyjs/node` and `@taffyjs/wasm`.
- The Yoga API group runs the same public Yoga-shaped benchmark source against `@taffyjs/yoga` and `yoga-layout`. A future `@taffyjs/yoga-wasm` target joins this group without changing the scenarios, runner, result format, or page structure.
- The Yoga API group runs the same public Yoga-shaped benchmark source against `@taffyjs/yoga`, `yoga-layout@3.2.1`, and `@taffyjs/yoga-wasm`.

Only implementations completing a semantically equivalent user transaction receive a speed comparison. Validation happens outside the timed interval and must never normalize the measured path. An unsupported implementation is reported as unsupported rather than receiving a transformed workload. Different API groups do not produce a shared ranking, and the benchmark never combines unrelated scenarios into an overall score or winner.

Expand All @@ -21,7 +19,7 @@ Every benchmark case is a scenario with a named question, input and scale, publi

Scenario timing follows the transaction being investigated. An initial-layout scenario may create the public tree, compute layout, and read the outputs the consumer needs. A persistent-tree scenario may mutate an existing tree, recompute it, and read the affected outputs without rebuilding the tree. A batch scenario may create, compute, read or serialize, and finish one complete item. Fixture parsing, result validation, and harness bookkeeping stay outside the timed interval unless they are themselves the public operation named by the scenario. A total is measured directly rather than reconstructed by adding separately aggregated phase results.

The exact initial scenario set remains open until it is researched and reviewed. It must be fixed before the official run whose result will be published; scenarios must not be selected or modified in response to which implementation wins.
The current suite covers a modeled coding-agent chat transaction plus one representative 1,000-leaf wide tree and 511-node deep tree for each public API group. Captured application trees may join later as ordinary scenarios when every target can execute the same supported public transaction. The suite must be fixed before the official run whose result will be published; scenarios must not be selected or modified in response to which implementation wins.

## Repository ownership

Expand Down Expand Up @@ -62,12 +60,12 @@ Local measurements belong under ignored `benchmarks/results/local/`. The website

The published dataset is a self-contained snapshot of the facts needed to interpret its numbers, including the scenario name and parameters, measured transaction, targets, results, environment, and TaffyJS revision. It retains enough sample evidence to support the displayed comparison. Its concrete schema and file encoding remain implementation details. The dataset is committed because it is evidence for a public performance claim, not a distributable build artifact.

The benchmark page presents every scenario at the same level. Each applicable scenario shows a Taffy API comparison and, separately, a Yoga API comparison. It does not display browser results, a Diagnostics section, an overall score, an original-data download interface, or an empty column for a package that does not exist. Target columns come from the result's target list, so adding `@taffyjs/yoga-wasm` later is a target addition rather than a page redesign.
The benchmark page presents every scenario at the same level. Each applicable scenario shows either a Taffy API comparison or a Yoga API comparison without ranking unlike APIs together. It does not display browser results, a Diagnostics section, an overall score, an original-data download interface, or an empty column for a package that does not exist. Target columns come from each comparison group's target list.

The page footer shows only the concise environment and source identity needed by a reader, including the Node.js version, operating system and benchmark machine, and TaffyJS commit.

## Delivery sequence

First select and document the initial scenarios and their exact transactions. Then implement the top-level benchmark workspace and the two comparison groups, prove completion and semantic equivalence outside timing, run the complete suite on the fixed Node.js benchmark host, update the retained dataset, and let the website render it. Before publication, review the result for comparison validity, measurement stability, and avoidable harness complexity.

The current delivery implements only the Taffy API comparison between `@taffyjs/node` and `@taffyjs/wasm`. The Yoga API comparison, captured Yoga application trees, and `@taffyjs/yoga-wasm` remain follow-up work after the relevant package pull requests merge. The current work may establish the shared result boundary, but it must not publish empty Yoga targets or manufacture a compatibility implementation in advance.
The current delivery implements the Taffy API comparison between `@taffyjs/node` and `@taffyjs/wasm` plus the Yoga API comparison between `@taffyjs/yoga`, Yoga 3.2.1, and `@taffyjs/yoga-wasm`. Captured Yoga application trees remain optional follow-up scenarios rather than a prerequisite for comparing the shipped public packages.
196 changes: 111 additions & 85 deletions apps/website/.vitepress/components/BenchmarkResults.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface BenchmarkScenario {

interface BenchmarkComparisonGroup {
readonly id: string;
readonly name: string;
readonly targets: readonly BenchmarkTarget[];
readonly scenarios: readonly BenchmarkScenario[];
}
Expand Down Expand Up @@ -63,35 +64,40 @@ if (report.source.dirty) {
throw new Error("Published benchmark results must come from a clean worktree");
}

const comparisonGroup = report.comparisonGroups.find(({ id }) => id === "taffy-api");
if (!comparisonGroup) {
throw new Error("Published benchmark results are missing the Taffy API comparison");
}

const nodeTarget = comparisonGroup.targets.find(
({ packageName }) => packageName === "@taffyjs/node",
);
if (!nodeTarget) {
throw new Error("Published benchmark results are missing @taffyjs/node");
if (report.comparisonGroups.length === 0) {
throw new Error("Published benchmark results contain no comparison groups");
}

const scenarios = comparisonGroup.scenarios.map((scenario) => {
const nodeResult = scenario.results.find(({ targetId }) => targetId === nodeTarget.id);
if (!nodeResult) {
throw new Error(`${scenario.id} is missing its @taffyjs/node result`);
const comparisonGroups = report.comparisonGroups.map((group) => {
const baselineTarget = group.targets[0];
if (!baselineTarget) {
throw new Error(`${group.id} contains no benchmark targets`);
}

return {
...scenario,
rows: comparisonGroup.targets.map((target) => {
const result = scenario.results.find(({ targetId }) => targetId === target.id);
if (!result) {
throw new Error(`${scenario.id} is missing its ${target.packageName} result`);
...group,
baselineTarget,
scenarios: group.scenarios.map((scenario) => {
const baselineResult = scenario.results.find(
({ targetId }) => targetId === baselineTarget.id,
);
if (!baselineResult) {
throw new Error(`${scenario.id} is missing its ${baselineTarget.packageName} result`);
}

return {
target,
result,
relativeThroughput: result.hz / nodeResult.hz,
...scenario,
rows: group.targets.map((target) => {
const result = scenario.results.find(({ targetId }) => targetId === target.id);
if (!result) {
throw new Error(`${scenario.id} is missing its ${target.packageName} result`);
}
return {
target,
result,
relativeThroughput: result.hz / baselineResult.hz,
};
}),
};
}),
};
Expand Down Expand Up @@ -140,69 +146,81 @@ const operatingSystem =
</p>

<section
v-for="scenario in scenarios"
:key="scenario.id"
class="benchmark-scenario"
:aria-labelledby="`${scenario.id}-heading`"
v-for="group in comparisonGroups"
:key="group.id"
class="benchmark-group"
:aria-labelledby="`${group.id}-heading`"
>
<h2 :id="`${scenario.id}-heading`">{{ scenario.name }}</h2>
<p class="benchmark-question">{{ scenario.question }}</p>
<p class="benchmark-description">{{ scenario.description }}</p>

<div class="benchmark-boundary">
<div class="benchmark-boundary-item">
<h3>Timed transaction</h3>
<p>{{ scenario.transaction }}</p>
</div>
<div class="benchmark-boundary-item">
<h3>Scale</h3>
<dl class="benchmark-parameters">
<div v-for="(value, name) in scenario.parameters" :key="name">
<dt>{{ formatParameterName(name) }}</dt>
<dd>{{ formatParameterValue(value) }}</dd>
</div>
</dl>
</div>
</div>
<h2 :id="`${group.id}-heading`">{{ group.name }}</h2>

<div
class="benchmark-table-region"
role="region"
:aria-label="`${scenario.name} results`"
tabindex="0"
<section
v-for="scenario in group.scenarios"
:key="scenario.id"
class="benchmark-scenario"
:aria-labelledby="`${scenario.id}-heading`"
>
<table>
<caption>
{{
scenario.name
}}
throughput and latency by package
</caption>
<thead>
<tr>
<th scope="col">Package</th>
<th scope="col">ops/s</th>
<th scope="col">Mean (ms)</th>
<th scope="col">Median (ms)</th>
<th scope="col">
Relative throughput
<span>Node = 1.00×</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in scenario.rows" :key="row.target.id">
<th scope="row">
<code>{{ row.target.packageName }}</code>
</th>
<td>{{ throughputFormatter.format(row.result.hz) }}</td>
<td>{{ durationFormatter.format(row.result.meanMs) }}</td>
<td>{{ durationFormatter.format(row.result.medianMs) }}</td>
<td>{{ throughputFormatter.format(row.relativeThroughput) }}×</td>
</tr>
</tbody>
</table>
</div>
<h3 :id="`${scenario.id}-heading`">{{ scenario.name }}</h3>
<p class="benchmark-question">{{ scenario.question }}</p>
<p class="benchmark-description">{{ scenario.description }}</p>

<div class="benchmark-boundary">
<div class="benchmark-boundary-item">
<h4>Timed transaction</h4>
<p>{{ scenario.transaction }}</p>
</div>
<div class="benchmark-boundary-item">
<h4>Scale</h4>
<dl class="benchmark-parameters">
<div v-for="(value, name) in scenario.parameters" :key="name">
<dt>{{ formatParameterName(name) }}</dt>
<dd>{{ formatParameterValue(value) }}</dd>
</div>
</dl>
</div>
</div>

<div
class="benchmark-table-region"
role="region"
:aria-label="`${group.name} ${scenario.name} results`"
tabindex="0"
>
<table>
<caption>
{{
group.name
}}
{{
scenario.name
}}
throughput and latency by package
</caption>
<thead>
<tr>
<th scope="col">Package</th>
<th scope="col">ops/s</th>
<th scope="col">Mean (ms)</th>
<th scope="col">Median (ms)</th>
<th scope="col">
Relative throughput
<span>{{ group.baselineTarget.label }} = 1.00×</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in scenario.rows" :key="row.target.id">
<th scope="row">
<code>{{ row.target.packageName }}</code>
</th>
<td>{{ throughputFormatter.format(row.result.hz) }}</td>
<td>{{ durationFormatter.format(row.result.meanMs) }}</td>
<td>{{ durationFormatter.format(row.result.medianMs) }}</td>
<td>{{ throughputFormatter.format(row.relativeThroughput) }}×</td>
</tr>
</tbody>
</table>
</div>
</section>
</section>

<section class="benchmark-run" aria-labelledby="benchmark-run-heading">
Expand Down Expand Up @@ -246,13 +264,21 @@ const operatingSystem =
color: var(--vp-c-text-2);
}

.benchmark-scenario {
.benchmark-group {
margin-top: 44px;
}

.benchmark-group > h2 {
margin: 0;
}

.benchmark-scenario {
margin-top: 28px;
padding-top: 34px;
border-top: 1px solid var(--vp-c-divider);
}

.benchmark-scenario h2 {
.benchmark-scenario > h3 {
margin: 0;
padding: 0;
border: 0;
Expand Down Expand Up @@ -288,7 +314,7 @@ const operatingSystem =
border-left: 1px solid var(--vp-c-divider);
}

.benchmark-boundary h3 {
.benchmark-boundary h4 {
margin: 0 0 8px;
font-size: 0.8rem;
font-weight: 700;
Expand Down
6 changes: 3 additions & 3 deletions apps/website/benchmarks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import BenchmarkResults from "../.vitepress/components/BenchmarkResults.vue";
</script>

# Node vs. Wasm
# Benchmarks

These results compare `@taffyjs/node` and `@taffyjs/wasm` in Node.js through the same public JavaScript API. Each timed end-to-end transaction starts after package import and includes public tree construction, JavaScript-to-native or JavaScript-to-Wasm data conversion, layout computation, and reading the requested layout results. The benchmark does not isolate engine time or subtract wrapper overhead.
These results compare complete public JavaScript transactions in Node.js. The Taffy API group compares `@taffyjs/node` with `@taffyjs/wasm`. The Yoga API group compares packages through the same Yoga-shaped API. Each transaction starts after package import and includes public tree construction, data conversion, layout computation, reading the requested layout results, and any lifecycle work required by that API.

Each scenario asks a different question and stands on its own. Relative throughput uses `@taffyjs/node` as the `1.00×` reference within that scenario; the results are not combined into an overall score or winner.
Each scenario asks a different question and stands on its own. Relative throughput uses the first target as the `1.00×` reference within its own API group. Results from different APIs or scenarios are not combined into an overall score or winner.

<BenchmarkResults />
Loading
Loading