Skip to content

Commit d04f69a

Browse files
authored
chore: add a benchmarking tool + single benchmark (#12092)
* chore: add benchmarking tool and single benchmark
1 parent a62dce3 commit d04f69a

File tree

6 files changed

+237
-15
lines changed

6 files changed

+237
-15
lines changed

benchmarking/benchmarks/mol_bench.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { fastest_test } from '../utils.js';
2+
import * as $ from '../../packages/svelte/src/internal/client/index.js';
3+
4+
/**
5+
* @param {number} n
6+
*/
7+
function fib(n) {
8+
if (n < 2) return 1;
9+
return fib(n - 1) + fib(n - 2);
10+
}
11+
12+
/**
13+
* @param {number} n
14+
*/
15+
function hard(n) {
16+
return n + fib(16);
17+
}
18+
19+
const numbers = Array.from({ length: 5 }, (_, i) => i);
20+
21+
function setup() {
22+
let res = [];
23+
const A = $.source(0);
24+
const B = $.source(0);
25+
const C = $.derived(() => ($.get(A) % 2) + ($.get(B) % 2));
26+
const D = $.derived(() => numbers.map((i) => ({ x: i + ($.get(A) % 2) - ($.get(B) % 2) })));
27+
const E = $.derived(() => hard($.get(C) + $.get(A) + $.get(D)[0].x));
28+
const F = $.derived(() => hard($.get(D)[2].x || $.get(B)));
29+
const G = $.derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[4].x + $.get(F));
30+
31+
const destroy = $.effect_root(() => {
32+
$.render_effect(() => {
33+
res.push(hard($.get(G)));
34+
});
35+
$.render_effect(() => {
36+
res.push($.get(G));
37+
});
38+
$.render_effect(() => {
39+
res.push(hard($.get(F)));
40+
});
41+
});
42+
43+
return {
44+
destroy,
45+
/**
46+
* @param {number} i
47+
*/
48+
run(i) {
49+
res.length = 0;
50+
$.flush_sync(() => {
51+
$.set(B, 1);
52+
$.set(A, 1 + i * 2);
53+
});
54+
$.flush_sync(() => {
55+
$.set(A, 2 + i * 2);
56+
$.set(B, 2);
57+
});
58+
}
59+
};
60+
}
61+
62+
export async function mol_bench() {
63+
// Do 10 loops to warm up JIT
64+
for (let i = 0; i < 10; i++) {
65+
const { run, destroy } = setup();
66+
run(0);
67+
destroy();
68+
}
69+
70+
const { run, destroy } = setup();
71+
72+
const { timing } = await fastest_test(10, () => {
73+
for (let i = 0; i < 1e4; i++) {
74+
run(i);
75+
}
76+
});
77+
78+
destroy();
79+
80+
return {
81+
benchmark: 'mol_bench',
82+
time: timing.time.toFixed(2),
83+
gc_time: timing.gc_time.toFixed(2)
84+
};
85+
}

benchmarking/run.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as $ from '../packages/svelte/src/internal/client/index.js';
2+
import { mol_bench } from './benchmarks/mol_bench.js';
3+
4+
const benchmarks = [mol_bench];
5+
6+
async function run_benchmarks() {
7+
const results = [];
8+
9+
$.push({}, true);
10+
for (const benchmark of benchmarks) {
11+
results.push(await benchmark());
12+
}
13+
$.pop();
14+
15+
// eslint-disable-next-line no-console
16+
console.log(results);
17+
}
18+
19+
run_benchmarks();

benchmarking/tsconfig.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"moduleResolution": "Bundler",
4+
"target": "ESNext",
5+
"module": "ESNext",
6+
"verbatimModuleSyntax": true,
7+
"isolatedModules": true,
8+
"resolveJsonModule": true,
9+
"sourceMap": true,
10+
"esModuleInterop": true,
11+
"skipLibCheck": true,
12+
"forceConsistentCasingInFileNames": true,
13+
"allowJs": true,
14+
"checkJs": true
15+
},
16+
"include": ["./run.js", "./utils.js", "./benchmarks"]
17+
}

benchmarking/utils.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { performance, PerformanceObserver } from 'node:perf_hooks';
2+
import v8 from 'v8-natives';
3+
4+
// Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking.
5+
6+
class GarbageTrack {
7+
track_id = 0;
8+
observer = new PerformanceObserver((list) => this.perf_entries.push(...list.getEntries()));
9+
perf_entries = [];
10+
periods = [];
11+
12+
watch(fn) {
13+
this.track_id++;
14+
const start = performance.now();
15+
const result = fn();
16+
const end = performance.now();
17+
this.periods.push({ track_id: this.track_id, start, end });
18+
19+
return { result, track_id: this.track_id };
20+
}
21+
22+
/**
23+
* @param {number} track_id
24+
*/
25+
async gcDuration(track_id) {
26+
await promise_delay(10);
27+
28+
const period = this.periods.find((period) => period.track_id === track_id);
29+
if (!period) {
30+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
31+
return Promise.reject('no period found');
32+
}
33+
34+
const entries = this.perf_entries.filter(
35+
(e) => e.startTime >= period.start && e.startTime < period.end
36+
);
37+
return entries.reduce((t, e) => e.duration + t, 0);
38+
}
39+
40+
destroy() {
41+
this.observer.disconnect();
42+
}
43+
44+
constructor() {
45+
this.observer.observe({ entryTypes: ['gc'] });
46+
}
47+
}
48+
49+
function promise_delay(timeout = 0) {
50+
return new Promise((resolve) => setTimeout(resolve, timeout));
51+
}
52+
53+
/**
54+
* @param {{ (): void; (): any; }} fn
55+
*/
56+
function run_timed(fn) {
57+
const start = performance.now();
58+
const result = fn();
59+
const time = performance.now() - start;
60+
return { result, time };
61+
}
62+
63+
/**
64+
* @param {() => void} fn
65+
*/
66+
async function run_tracked(fn) {
67+
v8.collectGarbage();
68+
const gc_track = new GarbageTrack();
69+
const { result: wrappedResult, track_id } = gc_track.watch(() => run_timed(fn));
70+
const gc_time = await gc_track.gcDuration(track_id);
71+
const { result, time } = wrappedResult;
72+
gc_track.destroy();
73+
return { result, timing: { time, gc_time } };
74+
}
75+
76+
/**
77+
* @param {number} times
78+
* @param {() => void} fn
79+
*/
80+
export async function fastest_test(times, fn) {
81+
const results = [];
82+
for (let i = 0; i < times; i++) {
83+
const run = await run_tracked(fn);
84+
results.push(run);
85+
}
86+
const fastest = results.reduce((a, b) => (a.timing.time < b.timing.time ? a : b));
87+
88+
return fastest;
89+
}

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
"test": "vitest run",
2424
"test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
2525
"changeset:version": "changeset version && pnpm -r generate:version && git add --all",
26-
"changeset:publish": "changeset publish"
26+
"changeset:publish": "changeset publish",
27+
"bench": "node --allow-natives-syntax ./benchmarking/run.js",
28+
"bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
2729
},
2830
"devDependencies": {
2931
"@changesets/cli": "^2.27.1",
@@ -41,6 +43,7 @@
4143
"prettier-plugin-svelte": "^3.1.2",
4244
"typescript": "^5.3.3",
4345
"typescript-eslint": "^8.0.0-alpha.20",
46+
"v8-natives": "^1.2.5",
4447
"vitest": "^1.2.1"
4548
},
4649
"pnpm": {

pnpm-lock.yaml

Lines changed: 23 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)