Skip to content

Commit 2e83cb2

Browse files
Marcus PousetteMarcus Pousette
authored andcommitted
perf(ci): add focused benchmark commands
1 parent 0351280 commit 2e83cb2

7 files changed

Lines changed: 148 additions & 21 deletions

File tree

.github/workflows/benchmarks.yml

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ on:
1212
suite:
1313
description: Benchmark suite
1414
required: true
15-
default: full
15+
default: quick
1616
type: choice
1717
options:
18-
- full
18+
- quick
1919
- web
20+
- full
2021

2122
permissions:
2223
contents: read
@@ -28,7 +29,7 @@ jobs:
2829
if: |
2930
(github.event_name == 'issue_comment' &&
3031
github.event.issue.pull_request &&
31-
startsWith(github.event.comment.body, '/bench') &&
32+
contains(fromJSON('["/bench","/bench quick","/bench web","/bench full"]'), github.event.comment.body) &&
3233
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
3334
) || (github.event_name == 'workflow_dispatch')
3435
runs-on: ubuntu-22.04
@@ -54,10 +55,21 @@ jobs:
5455
throw new Error('Unable to determine pull request number');
5556
}
5657
58+
const commentSuites = {
59+
'/bench': 'quick',
60+
'/bench quick': 'quick',
61+
'/bench web': 'web',
62+
'/bench full': 'full',
63+
};
64+
const commentBody = context.payload.comment?.body?.trim().toLowerCase();
5765
const suite = context.eventName === 'workflow_dispatch'
58-
? context.payload.inputs?.suite ?? 'full'
59-
: 'full';
60-
const benchmarkScripts = { full: 'benchmark', web: 'benchmark:web' };
66+
? context.payload.inputs?.suite ?? 'quick'
67+
: commentSuites[commentBody];
68+
const benchmarkScripts = {
69+
quick: 'benchmark:quick',
70+
full: 'benchmark',
71+
web: 'benchmark:web',
72+
};
6173
if (!Object.hasOwn(benchmarkScripts, suite)) {
6274
throw new Error(`Unsupported benchmark suite: ${suite}`);
6375
}
@@ -124,7 +136,6 @@ jobs:
124136
with:
125137
ref: ${{ steps.pr.outputs.base_sha }}
126138
path: bench-base
127-
fetch-depth: 0
128139
submodules: recursive
129140

130141
- name: Checkout head
@@ -133,9 +144,16 @@ jobs:
133144
with:
134145
ref: ${{ steps.pr.outputs.head_sha }}
135146
path: bench-head
136-
fetch-depth: 0
137147
submodules: recursive
138148

149+
- name: Checkout benchmark tools
150+
if: steps.pr.outputs.same_repo == 'true' && steps.pr.outputs.suite == 'quick'
151+
uses: actions/checkout@v4
152+
with:
153+
ref: ${{ github.workflow_sha }}
154+
path: bench-tools
155+
sparse-checkout: scripts
156+
139157
- name: Use Node.js 20.x
140158
if: steps.pr.outputs.same_repo == 'true'
141159
uses: actions/setup-node@v4
@@ -151,8 +169,6 @@ jobs:
151169
- name: Install Rust toolchain
152170
if: steps.pr.outputs.same_repo == 'true'
153171
uses: dtolnay/rust-toolchain@stable
154-
with:
155-
components: rustfmt, clippy
156172

157173
- name: Add wasm targets
158174
if: steps.pr.outputs.same_repo == 'true'
@@ -219,8 +235,16 @@ jobs:
219235
run: |
220236
set -o pipefail
221237
status=0
222-
env -u CI pnpm run "$BENCHMARK_SCRIPT" 2>&1 | tee benchmark.log || status=$?
223-
pnpm run benchmark:aggregate || status=$?
238+
if [ "$BENCHMARK_SCRIPT" = "benchmark:quick" ]; then
239+
env -u CI TREECRDT_BENCH_ROOT="$PWD" \
240+
node "$GITHUB_WORKSPACE/bench-tools/scripts/run-quick-bench.mjs" \
241+
2>&1 | tee benchmark.log || status=$?
242+
else
243+
env -u CI pnpm run "$BENCHMARK_SCRIPT" 2>&1 | tee benchmark.log || status=$?
244+
fi
245+
if [ ! -f benchmarks/summary.json ]; then
246+
pnpm run benchmark:aggregate || status=$?
247+
fi
224248
exit "$status"
225249
226250
- name: Run benchmarks (head)
@@ -234,8 +258,16 @@ jobs:
234258
run: |
235259
set -o pipefail
236260
status=0
237-
env -u CI pnpm run "$BENCHMARK_SCRIPT" 2>&1 | tee benchmark.log || status=$?
238-
pnpm run benchmark:aggregate || status=$?
261+
if [ "$BENCHMARK_SCRIPT" = "benchmark:quick" ]; then
262+
env -u CI TREECRDT_BENCH_ROOT="$PWD" \
263+
node "$GITHUB_WORKSPACE/bench-tools/scripts/run-quick-bench.mjs" \
264+
2>&1 | tee benchmark.log || status=$?
265+
else
266+
env -u CI pnpm run "$BENCHMARK_SCRIPT" 2>&1 | tee benchmark.log || status=$?
267+
fi
268+
if [ ! -f benchmarks/summary.json ]; then
269+
pnpm run benchmark:aggregate || status=$?
270+
fi
239271
exit "$status"
240272
241273
- name: Upload benchmark artifacts
@@ -342,8 +374,9 @@ jobs:
342374
const baseOutcome = '${{ steps.benchmark_base.outcome }}';
343375
const headOutcome = '${{ steps.benchmark_head.outcome }}';
344376
const benchmarkFailed = [baseOutcome, headOutcome].includes('failure');
345-
const triggerLabel =
346-
context.eventName === 'workflow_dispatch' ? '`workflow_dispatch`' : '`/bench`';
377+
const triggerLabel = context.eventName === 'workflow_dispatch'
378+
? '`workflow_dispatch`'
379+
: `\`${context.payload.comment.body.trim()}\``;
347380
348381
const parts = [
349382
marker,

docs/BENCHMARKS.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pnpm benchmark:sync:help
1414
Useful top-level entrypoints:
1515

1616
```sh
17+
pnpm benchmark:quick
1718
pnpm benchmark
1819
pnpm benchmark:sqlite-node
1920
pnpm benchmark:sqlite-node:ops
@@ -29,7 +30,18 @@ pnpm benchmark:wasm
2930
pnpm benchmark:postgres
3031
```
3132

32-
`pnpm benchmark` writes JSON results under `benchmarks/`.
33+
Both `pnpm benchmark:quick` and `pnpm benchmark` write JSON results under `benchmarks/`.
34+
The quick suite samples every primary runtime at a representative scale; the full suite retains the exhaustive size and iteration matrix.
35+
36+
### Pull Request Comparisons
37+
38+
Repository members can trigger base/head comparisons from a pull request:
39+
40+
- `/bench` or `/bench quick`: representative browser/OPFS, native SQLite, direct sync, WASM, and Rust core coverage
41+
- `/bench web`: full browser memory and OPFS coverage
42+
- `/bench full`: the exhaustive suite
43+
44+
Use quick for routine feedback and full when confirming a broad or performance-sensitive change.
3345

3446
## Which Benchmark Answers What?
3547

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"build": "pnpm -r --if-present run build",
2424
"test:browser": "pnpm -C packages/treecrdt-wa-sqlite/e2e test:e2e",
2525
"test:native-node": "pnpm -C packages/sync-protocol/material/sqlite run test && pnpm -C packages/treecrdt-sqlite-node run test && pnpm -C packages/treecrdt-wa-sqlite run test",
26-
"benchmark": "rm -rf benchmarks && mkdir -p benchmarks && pnpm run benchmark:web && pnpm run benchmark:sqlite-node:ops && pnpm run benchmark:sqlite-node:note-paths && pnpm run benchmark:sync:direct && pnpm run benchmark:wasm && pnpm run benchmark:postgres && pnpm run benchmark:core && pnpm run benchmark:aggregate",
26+
"benchmark": "rm -rf benchmarks && mkdir -p benchmarks && pnpm run benchmark:web && pnpm run benchmark:sqlite-node && pnpm run benchmark:wasm && pnpm run benchmark:postgres && pnpm run benchmark:core && pnpm run benchmark:aggregate",
27+
"benchmark:quick": "node scripts/run-quick-bench.mjs",
2728
"benchmark:sqlite-node": "pnpm run benchmark:sqlite-node:ops && pnpm run benchmark:sqlite-node:note-paths && pnpm run benchmark:sqlite-node:sync",
2829
"benchmark:core": "cargo bench -p treecrdt-core --bench core --features bench",
2930
"benchmark:aggregate": "node scripts/aggregate-bench.mjs",

packages/treecrdt-benchmark/src/stats.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
function readEnv(name: string): string | undefined {
2-
const env = (globalThis as any)?.process?.env as Record<string, string | undefined> | undefined;
2+
const env = ((globalThis as any)?.process?.env ?? (import.meta as any).env) as
3+
| Record<string, string | undefined>
4+
| undefined;
35
const raw = env?.[name];
46
return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
57
}

packages/treecrdt-core/benches/core.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ fn run_benchmark(replica: &ReplicaId, count: u64) -> f64 {
7272
fn main() {
7373
let mut out_dir: Option<PathBuf> = None;
7474
let mut custom_config: Option<Vec<(u64, u64)>> = None;
75+
let mut custom_iterations: Option<u64> = None;
7576
for arg in env::args().skip(1) {
7677
if let Some(val) = arg.strip_prefix("--count=") {
7778
let count = val.parse().unwrap_or(500);
@@ -85,17 +86,25 @@ fn main() {
8586
if !parsed.is_empty() {
8687
custom_config = Some(parsed);
8788
}
89+
} else if let Some(val) = arg.strip_prefix("--iterations=") {
90+
custom_iterations = val.parse::<u64>().ok().filter(|&iterations| iterations > 0);
8891
} else if let Some(val) = arg.strip_prefix("--out-dir=") {
8992
out_dir = Some(PathBuf::from(val));
9093
}
9194
}
9295

93-
let config = custom_config.as_deref().unwrap_or(BENCH_CONFIG);
96+
let mut config = custom_config.unwrap_or_else(|| BENCH_CONFIG.to_vec());
97+
if let Some(iterations) = custom_iterations {
98+
for (_, configured_iterations) in &mut config {
99+
*configured_iterations = iterations;
100+
}
101+
}
102+
94103
let out_dir = out_dir.unwrap_or_else(default_out_dir);
95104
fs::create_dir_all(&out_dir).expect("mkdirs");
96105

97106
let replica = ReplicaId::new(b"core");
98-
for &(count, iterations) in config {
107+
for (count, iterations) in config {
99108
let (duration_ms, iterations_opt, avg_duration_ms) = if iterations > 1 {
100109
let mut durations: Vec<f64> =
101110
(0..iterations).map(|_| run_benchmark(&replica, count)).collect();

packages/treecrdt-wa-sqlite/e2e/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const vendorWaSqliteRoot = path.join(vendorPkgRoot, 'wa-sqlite');
1515
const vendorDistRoot = path.join(vendorPkgRoot, 'dist');
1616

1717
export default defineConfig({
18+
envPrefix: ['VITE_', 'BENCH_', 'SYNC_BENCH_'],
1819
plugins: [
1920
treecrdtWaSqliteAssets({ outDirs: ['public/wa-sqlite', 'public/base-path/wa-sqlite'] }),
2021
react(),

scripts/run-quick-bench.mjs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { spawnSync } from 'node:child_process';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
5+
import { repoRootFromImportMeta } from './repo-root.mjs';
6+
7+
const repoRoot = process.env.TREECRDT_BENCH_ROOT
8+
? path.resolve(process.env.TREECRDT_BENCH_ROOT)
9+
: repoRootFromImportMeta(import.meta.url, 1);
10+
const quickEnv = {
11+
BENCH_ITERATIONS: '3',
12+
BENCH_WARMUP: '1',
13+
SYNC_BENCH_SIZES: '1000',
14+
};
15+
16+
const commands = [
17+
['pnpm', ['run', 'benchmark:web']],
18+
['pnpm', ['-C', 'packages/treecrdt-sqlite-node', 'run', 'benchmark:ops', '--', '--count=1000']],
19+
[
20+
'pnpm',
21+
['-C', 'packages/treecrdt-sqlite-node', 'run', 'benchmark:note-paths', '--', '--count=10000'],
22+
],
23+
[
24+
process.execPath,
25+
['scripts/run-sync-bench.mjs', 'direct', '--count=1000', '--iterations=3', '--warmup=1'],
26+
],
27+
[
28+
'pnpm',
29+
[
30+
'-C',
31+
'packages/treecrdt-wasm-js',
32+
'run',
33+
'benchmark',
34+
'--',
35+
'--sizes=1000',
36+
'--workloads=insert-move,insert-chain',
37+
],
38+
],
39+
[
40+
'cargo',
41+
[
42+
'bench',
43+
'-p',
44+
'treecrdt-core',
45+
'--bench',
46+
'core',
47+
'--features',
48+
'bench',
49+
'--',
50+
'--count=1000',
51+
'--iterations=3',
52+
],
53+
],
54+
['pnpm', ['run', 'benchmark:aggregate']],
55+
];
56+
57+
fs.rmSync(path.join(repoRoot, 'benchmarks'), { recursive: true, force: true });
58+
fs.mkdirSync(path.join(repoRoot, 'benchmarks'), { recursive: true });
59+
60+
for (const [command, args] of commands) {
61+
console.log(`\n[benchmark:quick] ${command} ${args.join(' ')}`);
62+
const result = spawnSync(command, args, {
63+
cwd: repoRoot,
64+
env: { ...process.env, ...quickEnv },
65+
stdio: 'inherit',
66+
});
67+
if (result.error) throw result.error;
68+
if (result.status !== 0) process.exit(result.status ?? 1);
69+
}

0 commit comments

Comments
 (0)