diff --git a/benchmarking/benchmarks/mol_bench.js b/benchmarking/benchmarks/mol_bench.js new file mode 100644 index 0000000000..f98521c809 --- /dev/null +++ b/benchmarking/benchmarks/mol_bench.js @@ -0,0 +1,85 @@ +import { fastest_test } from '../utils.js'; +import * as $ from '../../packages/svelte/src/internal/client/index.js'; + +/** + * @param {number} n + */ +function fib(n) { + if (n < 2) return 1; + return fib(n - 1) + fib(n - 2); +} + +/** + * @param {number} n + */ +function hard(n) { + return n + fib(16); +} + +const numbers = Array.from({ length: 5 }, (_, i) => i); + +function setup() { + let res = []; + const A = $.source(0); + const B = $.source(0); + const C = $.derived(() => ($.get(A) % 2) + ($.get(B) % 2)); + const D = $.derived(() => numbers.map((i) => ({ x: i + ($.get(A) % 2) - ($.get(B) % 2) }))); + const E = $.derived(() => hard($.get(C) + $.get(A) + $.get(D)[0].x)); + const F = $.derived(() => hard($.get(D)[2].x || $.get(B))); + const G = $.derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[4].x + $.get(F)); + + const destroy = $.effect_root(() => { + $.render_effect(() => { + res.push(hard($.get(G))); + }); + $.render_effect(() => { + res.push($.get(G)); + }); + $.render_effect(() => { + res.push(hard($.get(F))); + }); + }); + + return { + destroy, + /** + * @param {number} i + */ + run(i) { + res.length = 0; + $.flush_sync(() => { + $.set(B, 1); + $.set(A, 1 + i * 2); + }); + $.flush_sync(() => { + $.set(A, 2 + i * 2); + $.set(B, 2); + }); + } + }; +} + +export async function mol_bench() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(0); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1e4; i++) { + run(i); + } + }); + + destroy(); + + return { + benchmark: 'mol_bench', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/run.js b/benchmarking/run.js new file mode 100644 index 0000000000..060b982a85 --- /dev/null +++ b/benchmarking/run.js @@ -0,0 +1,19 @@ +import * as $ from '../packages/svelte/src/internal/client/index.js'; +import { mol_bench } from './benchmarks/mol_bench.js'; + +const benchmarks = [mol_bench]; + +async function run_benchmarks() { + const results = []; + + $.push({}, true); + for (const benchmark of benchmarks) { + results.push(await benchmark()); + } + $.pop(); + + // eslint-disable-next-line no-console + console.log(results); +} + +run_benchmarks(); diff --git a/benchmarking/tsconfig.json b/benchmarking/tsconfig.json new file mode 100644 index 0000000000..81fe19744a --- /dev/null +++ b/benchmarking/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "moduleResolution": "Bundler", + "target": "ESNext", + "module": "ESNext", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "checkJs": true + }, + "include": ["./run.js", "./utils.js", "./benchmarks"] +} diff --git a/benchmarking/utils.js b/benchmarking/utils.js new file mode 100644 index 0000000000..d9594c1f03 --- /dev/null +++ b/benchmarking/utils.js @@ -0,0 +1,89 @@ +import { performance, PerformanceObserver } from 'node:perf_hooks'; +import v8 from 'v8-natives'; + +// Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking. + +class GarbageTrack { + track_id = 0; + observer = new PerformanceObserver((list) => this.perf_entries.push(...list.getEntries())); + perf_entries = []; + periods = []; + + watch(fn) { + this.track_id++; + const start = performance.now(); + const result = fn(); + const end = performance.now(); + this.periods.push({ track_id: this.track_id, start, end }); + + return { result, track_id: this.track_id }; + } + + /** + * @param {number} track_id + */ + async gcDuration(track_id) { + await promise_delay(10); + + const period = this.periods.find((period) => period.track_id === track_id); + if (!period) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return Promise.reject('no period found'); + } + + const entries = this.perf_entries.filter( + (e) => e.startTime >= period.start && e.startTime < period.end + ); + return entries.reduce((t, e) => e.duration + t, 0); + } + + destroy() { + this.observer.disconnect(); + } + + constructor() { + this.observer.observe({ entryTypes: ['gc'] }); + } +} + +function promise_delay(timeout = 0) { + return new Promise((resolve) => setTimeout(resolve, timeout)); +} + +/** + * @param {{ (): void; (): any; }} fn + */ +function run_timed(fn) { + const start = performance.now(); + const result = fn(); + const time = performance.now() - start; + return { result, time }; +} + +/** + * @param {() => void} fn + */ +async function run_tracked(fn) { + v8.collectGarbage(); + const gc_track = new GarbageTrack(); + const { result: wrappedResult, track_id } = gc_track.watch(() => run_timed(fn)); + const gc_time = await gc_track.gcDuration(track_id); + const { result, time } = wrappedResult; + gc_track.destroy(); + return { result, timing: { time, gc_time } }; +} + +/** + * @param {number} times + * @param {() => void} fn + */ +export async function fastest_test(times, fn) { + const results = []; + for (let i = 0; i < times; i++) { + const run = await run_tracked(fn); + results.push(run); + } + const fastest = results.reduce((a, b) => (a.timing.time < b.timing.time ? a : b)); + + return fastest; +} diff --git a/package.json b/package.json index bb50cef2d5..27b844bc39 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "test": "vitest run", "test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json", "changeset:version": "changeset version && pnpm -r generate:version && git add --all", - "changeset:publish": "changeset publish" + "changeset:publish": "changeset publish", + "bench": "node --allow-natives-syntax ./benchmarking/run.js", + "bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js" }, "devDependencies": { "@changesets/cli": "^2.27.1", @@ -41,6 +43,7 @@ "prettier-plugin-svelte": "^3.1.2", "typescript": "^5.3.3", "typescript-eslint": "^8.0.0-alpha.20", + "v8-natives": "^1.2.5", "vitest": "^1.2.1" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e63c570df..7c61c77e45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 2.27.1 '@sveltejs/eslint-config': specifier: ^7.0.1 - version: 7.0.1(@stylistic/eslint-plugin-js@1.8.0(eslint@9.0.0))(eslint-config-prettier@9.1.0(eslint@9.0.0))(eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.144))(eslint-plugin-unicorn@52.0.0(eslint@9.0.0))(eslint@9.0.0)(typescript-eslint@8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3))(typescript@5.3.3) + version: 7.0.1(@stylistic/eslint-plugin-js@1.8.0(eslint@9.0.0))(eslint-config-prettier@9.1.0(eslint@9.0.0))(eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.158))(eslint-plugin-unicorn@52.0.0(eslint@9.0.0))(eslint@9.0.0)(typescript-eslint@8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3))(typescript@5.3.3) '@svitejs/changesets-changelog-github-compact': specifier: ^1.1.0 version: 1.1.0 @@ -49,13 +49,16 @@ importers: version: 3.2.4 prettier-plugin-svelte: specifier: ^3.1.2 - version: 3.1.2(prettier@3.2.4)(svelte@5.0.0-next.144) + version: 3.1.2(prettier@3.2.4)(svelte@5.0.0-next.158) typescript: specifier: ^5.3.3 version: 5.3.3 typescript-eslint: specifier: ^8.0.0-alpha.20 version: 8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3) + v8-natives: + specifier: ^1.2.5 + version: 1.2.5 vitest: specifier: ^1.2.1 version: 1.2.1(@types/node@20.11.5)(jsdom@22.0.0)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) @@ -4575,8 +4578,8 @@ packages: resolution: {integrity: sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==} engines: {node: '>=16'} - svelte@5.0.0-next.144: - resolution: {integrity: sha512-akjtRBHzaLa1XdMv9tBGkXE5N2JaRc3gL+ZIctjc9Gew9DF7NxGTlxXq+HR9yUV7Lsg4o9ltMfkxz8H3K7piNQ==} + svelte@5.0.0-next.158: + resolution: {integrity: sha512-QRmXxHByWntyWqLtzjNsBbNT89F2yA7aWPp9M9l9a6/PAE3gmQh6+qoVPgrxMR7iiFgpwh5ZU9Bm25j3IhGicQ==} engines: {node: '>=18'} symbol-tree@3.2.4: @@ -4829,6 +4832,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + v8-natives@1.2.5: + resolution: {integrity: sha512-CVNliz6KF2yet3HBIkbFJKZmjlt95C8dsNZDnwoS6X98+QJRpsSz9uxo3TziBqdyJQkWwfD3VG9lRzsQNvF24Q==} + engines: {node: '>= 0.6.0'} + v8-to-istanbul@9.2.0: resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} engines: {node: '>=10.12.0'} @@ -6453,12 +6460,12 @@ snapshots: - encoding - supports-color - '@sveltejs/eslint-config@7.0.1(@stylistic/eslint-plugin-js@1.8.0(eslint@9.0.0))(eslint-config-prettier@9.1.0(eslint@9.0.0))(eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.144))(eslint-plugin-unicorn@52.0.0(eslint@9.0.0))(eslint@9.0.0)(typescript-eslint@8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3))(typescript@5.3.3)': + '@sveltejs/eslint-config@7.0.1(@stylistic/eslint-plugin-js@1.8.0(eslint@9.0.0))(eslint-config-prettier@9.1.0(eslint@9.0.0))(eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.158))(eslint-plugin-unicorn@52.0.0(eslint@9.0.0))(eslint@9.0.0)(typescript-eslint@8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3))(typescript@5.3.3)': dependencies: '@stylistic/eslint-plugin-js': 1.8.0(eslint@9.0.0) eslint: 9.0.0 eslint-config-prettier: 9.1.0(eslint@9.0.0) - eslint-plugin-svelte: 2.38.0(eslint@9.0.0)(svelte@5.0.0-next.144) + eslint-plugin-svelte: 2.38.0(eslint@9.0.0)(svelte@5.0.0-next.158) eslint-plugin-unicorn: 52.0.0(eslint@9.0.0) globals: 15.0.0 typescript: 5.3.3 @@ -7570,7 +7577,7 @@ snapshots: eslint-plugin-lube@0.4.3: {} - eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.144): + eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.158): dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) '@jridgewell/sourcemap-codec': 1.4.15 @@ -7584,9 +7591,9 @@ snapshots: postcss-safe-parser: 6.0.0(postcss@8.4.38) postcss-selector-parser: 6.0.16 semver: 7.6.0 - svelte-eslint-parser: 0.35.0(svelte@5.0.0-next.144) + svelte-eslint-parser: 0.35.0(svelte@5.0.0-next.158) optionalDependencies: - svelte: 5.0.0-next.144 + svelte: 5.0.0-next.158 transitivePeerDependencies: - supports-color - ts-node @@ -9094,10 +9101,10 @@ snapshots: prettier: 3.2.4 svelte: 4.2.9 - prettier-plugin-svelte@3.1.2(prettier@3.2.4)(svelte@5.0.0-next.144): + prettier-plugin-svelte@3.1.2(prettier@3.2.4)(svelte@5.0.0-next.158): dependencies: prettier: 3.2.4 - svelte: 5.0.0-next.144 + svelte: 5.0.0-next.158 prettier@2.8.8: {} @@ -9726,7 +9733,7 @@ snapshots: - stylus - sugarss - svelte-eslint-parser@0.35.0(svelte@5.0.0-next.144): + svelte-eslint-parser@0.35.0(svelte@5.0.0-next.158): dependencies: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -9734,7 +9741,7 @@ snapshots: postcss: 8.4.38 postcss-scss: 4.0.9(postcss@8.4.38) optionalDependencies: - svelte: 5.0.0-next.144 + svelte: 5.0.0-next.158 svelte-hmr@0.16.0(svelte@4.2.9): dependencies: @@ -9809,7 +9816,7 @@ snapshots: magic-string: 0.30.5 periscopic: 3.1.0 - svelte@5.0.0-next.144: + svelte@5.0.0-next.158: dependencies: '@ampproject/remapping': 2.2.1 '@jridgewell/sourcemap-codec': 1.4.15 @@ -10070,6 +10077,8 @@ snapshots: utils-merge@1.0.1: {} + v8-natives@1.2.5: {} + v8-to-istanbul@9.2.0: dependencies: '@jridgewell/trace-mapping': 0.3.22