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
61 changes: 61 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Runs the tachometer benchmark matrix and publishes results to the
# `gh-pages` branch's `bench/` history (dispatch: "bench" track), read by
# github-action-benchmark's own chart page and by pages.yml's copy into
# `dist/bench/`.
name: bench

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: bench-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: write

env:
DENO_VERSION: "2.9.5"
WASM_TOOLS_VERSION: "1.247.0"
JUST_VERSION: "1.54.0"

jobs:
bench:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.98.0
targets: wasm32-wasip2
components: clippy
- uses: denoland/setup-deno@v2
with:
deno-version: ${{ env.DENO_VERSION }}
- uses: taiki-e/install-action@v2
with:
tool: just@${{ env.JUST_VERSION }},wasm-tools@${{ env.WASM_TOOLS_VERSION }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
.
guests/web-sys
- uses: actions/setup-node@v4
with:
node-version: "24"
- run: just components
- run: just site
- run: deno run -A bench/run.ts --sample-size 25
- uses: benchmark-action/github-action-benchmark@v1
with:
tool: customSmallerIsBetter
output-file-path: bench/results/benchmark.json
gh-pages-branch: gh-pages
benchmark-data-dir-path: bench
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
alert-threshold: "150%"
fail-on-alert: false
comment-on-alert: false
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
with:
workspaces: |
.
guests/dominator
guests/web-sys
- run: just check
- run: just test
- run: just components
Expand All @@ -51,3 +51,4 @@ jobs:
# step is the browser binary only and is a no-op after this.
- run: npx -y playwright@1.58 install --with-deps chromium
- run: just e2e
- run: just bench-wire
11 changes: 10 additions & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,18 @@ jobs:
with:
workspaces: |
.
guests/dominator
guests/web-sys
- run: just components
- run: just site
# gh-pages branch may not exist yet on the very first run —
# continue-on-error, and the copy step below no-ops via `2>/dev/null`
# if the checkout produced nothing.
- uses: actions/checkout@v7
continue-on-error: true
with:
ref: gh-pages
path: gh-pages-data
- run: cp -r gh-pages-data/bench dist/bench 2>/dev/null || true
- uses: actions/upload-pages-artifact@v3
with:
path: dist
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ dist/
.playwright/
test-results/
playwright-report/
bench/results/
bench/tachometer.json
8 changes: 8 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"crates/stream-dom-guest",
"crates/stream-dom-dioxus",
"guests/dioxus/todomvc",
"guests/dioxus/bench",
]
# The Dominator guest is its own workspace: it `[patch]`es wasm-bindgen,
# js-sys, web-sys and wasm-bindgen-futures with the fake-DOM shims, and a
Expand Down
106 changes: 106 additions & 0 deletions bench/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Runs the tachometer benchmark matrix against `dist/` and converts its
// results into github-action-benchmark's `customSmallerIsBetter` format
// (dispatch: "bench" track).
//
// deno run -A bench/run.ts [--sample-size N] [--filter substring]
// [--chrome-binary path]
//
// Requires `just site` first (this does not build `dist/` itself, unlike
// `just bench`, which runs `site` before this).

import { fromFileUrl, join } from "@std/path";
import { generateConfig } from "./tachometer.ts";

const benchDir = fromFileUrl(new URL(".", import.meta.url));
const resultsDir = join(benchDir, "results");

interface Args {
sampleSize?: number;
filter?: string;
chromeBinary?: string;
}

function parseArgs(argv: string[]): Args {
const args: Args = {};
const rest = [...argv];
while (rest.length) {
const a = rest.shift()!;
if (a === "--sample-size") args.sampleSize = Number(rest.shift());
else if (a === "--filter") args.filter = rest.shift();
else if (a === "--chrome-binary") args.chromeBinary = rest.shift();
else {
console.error(`unknown argument: ${a}`);
Deno.exit(2);
}
}
return args;
}

const args = parseArgs(Deno.args);

const config = generateConfig({
filter: args.filter,
sampleSize: args.sampleSize,
chromeBinary: args.chromeBinary,
});
const configPath = join(benchDir, "tachometer.json");
await Deno.writeTextFile(configPath, JSON.stringify(config, null, 2) + "\n");
console.log(`${configPath}: ${config.benchmarks.length} benchmarks`);

await Deno.mkdir(resultsDir, { recursive: true });
const tachometerJsonPath = join(resultsDir, "tachometer.json");

const proc = new Deno.Command("npx", {
args: [
"-y",
"tachometer@0.7.1",
"--config",
configPath,
"--json-file",
tachometerJsonPath,
],
stdout: "inherit",
stderr: "inherit",
});
const result = await proc.output();
if (!result.success) {
console.error("tachometer run failed");
Deno.exit(1);
}

interface TachometerResult {
benchmarks: Array<{
name: string;
mean: { low: number; high: number };
}>;
}

const tachometerResults: TachometerResult = JSON.parse(
await Deno.readTextFile(tachometerJsonPath),
);

interface CustomSmallerIsBetterEntry {
name: string;
unit: string;
value: number;
range: string;
}

const benchmarkJson: CustomSmallerIsBetterEntry[] = tachometerResults
.benchmarks.map((b) => {
const value = (b.mean.low + b.mean.high) / 2;
const half = (b.mean.high - b.mean.low) / 2;
return {
name: b.name,
unit: "ms",
value,
range: `± ${half.toFixed(3)}`,
};
});

const benchmarkJsonPath = join(resultsDir, "benchmark.json");
await Deno.writeTextFile(
benchmarkJsonPath,
JSON.stringify(benchmarkJson, null, 2) + "\n",
);
console.log(`${benchmarkJsonPath}: ${benchmarkJson.length} entries`);
98 changes: 98 additions & 0 deletions bench/tachometer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Generates `bench/tachometer.json` — the full producers x receivers x
// transports x ops matrix — for tachometer@0.7.1 (dispatch: "bench"
// track). Sampling/statistics belong to tachometer; this only enumerates
// benchmark URLs and measurement config.
//
// CONTRACT (see web/bench.ts's header comment for the full account):
// `measurement: "global"` polls `window.tachometerResult` (a plain
// assigned number), which is what a static `dist/` page can support —
// not the `{mode: "callback"}` object form the dispatch's prose
// describes, which needs tachometer's own dev-server-injected
// `/bench.js` `start()`/`stop()` module. Verified against
// tachometer@0.7.1's config.schema.json and README.md "Measurement
// modes" > "Global result".

export const PRODUCERS = ["dioxus-bench", "dominator-bench"] as const;
export const RECEIVERS = ["native", "remote"] as const;
export const TRANSPORTS = ["direct", "chunked"] as const;
export const OPS = [
"create-1k",
"replace-1k",
"create-10k",
"append-1k",
"update-every-10th",
"select-row",
"swap-rows",
"remove-row",
"clear",
] as const;

export interface TachometerBenchmark {
name: string;
url: string;
measurement: "global";
browser: { name: "chrome"; headless: true; binary?: string };
}

export interface TachometerConfig {
root: string;
sampleSize?: number;
benchmarks: TachometerBenchmark[];
}

export interface GenerateOptions {
/** Substring filter against a benchmark's `name`
* (`<producer>/<receiver>/<transport>/<op>`). */
filter?: string;
sampleSize?: number;
/** `browser.binary` passthrough — a local Chrome/Chromium binary path
* (dispatch: "support `--chrome-binary` passthrough"). */
chromeBinary?: string;
}

export function generateConfig(opts: GenerateOptions = {}): TachometerConfig {
const benchmarks: TachometerBenchmark[] = [];
for (const producer of PRODUCERS) {
for (const receiver of RECEIVERS) {
for (const transport of TRANSPORTS) {
for (const op of OPS) {
const name = `${producer}/${receiver}/${transport}/${op}`;
if (opts.filter && !name.includes(opts.filter)) continue;
benchmarks.push({
name,
// Resolved by tachometer relative to *this config file's own
// directory* (bench/), not `root` — config.ts's
// `urlFromLocalPath`/`parseBenchmark` compute
// `path.resolve(dirname(configFilePath), urlPath)` and then
// check the result falls under `root` (itself resolved the
// same way). `root` below is `../dist` for the same reason:
// both must land on the same `dist/` directory from bench/'s
// perspective.
url:
`../dist/bench.html?app=${producer}&receiver=${receiver}&transport=${transport}&op=${op}`,
measurement: "global",
browser: {
name: "chrome",
headless: true,
...(opts.chromeBinary ? { binary: opts.chromeBinary } : {}),
},
});
}
}
}
}
return {
root: "../dist",
...(opts.sampleSize ? { sampleSize: opts.sampleSize } : {}),
benchmarks,
};
}

if (import.meta.main) {
const config = generateConfig();
await Deno.writeTextFile(
new URL("./tachometer.json", import.meta.url),
JSON.stringify(config, null, 2) + "\n",
);
console.log(`bench/tachometer.json: ${config.benchmarks.length} benchmarks`);
}
Loading
Loading