From 16e9c70d1e97ef37800a67086b821e4bc77aa634 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:47:01 +0100 Subject: [PATCH 1/7] Version Packages (#17488) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/social-foxes-pull.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/social-foxes-pull.md diff --git a/.changeset/social-foxes-pull.md b/.changeset/social-foxes-pull.md deleted file mode 100644 index d164d43f7b..0000000000 --- a/.changeset/social-foxes-pull.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: trigger `selectedcontent` reactivity diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index e222997351..d019b7883f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.47.1 + +### Patch Changes + +- fix: trigger `selectedcontent` reactivity ([#17486](https://github.com/sveltejs/svelte/pull/17486)) + ## 5.47.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 19e37aaa93..38cac7b1cd 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.47.0", + "version": "5.47.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 38b2a77f4a..a5360f7195 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.47.0'; +export const VERSION = '5.47.1'; export const PUBLIC_VERSION = '5'; From 2eb54f57ae9b2411a40f10266133356ab647d907 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 Jan 2026 18:26:51 -0500 Subject: [PATCH 2/7] chore: tidy up benchmark code (#17490) * add create_test utility * add missing import * DRY * DRY out * mol (this affects benchmark times, because create_test uses 1e3 iterations whereas the old code used 1e4) * move utils.js * WIP * DRY * reduce indirection * more * tidy up * tidy up * colocate * DRY out --- benchmarking/benchmarks/reactivity/index.js | 46 +- .../reactivity/kairo/kairo_avoidable.js | 91 ---- .../reactivity/kairo/kairo_broad.js | 97 ---- .../benchmarks/reactivity/kairo/kairo_deep.js | 97 ---- .../reactivity/kairo/kairo_diamond.js | 101 ---- .../benchmarks/reactivity/kairo/kairo_mux.js | 94 ---- .../reactivity/kairo/kairo_repeated.js | 98 ---- .../reactivity/kairo/kairo_triangle.js | 111 ----- .../reactivity/kairo/kairo_unstable.js | 97 ---- .../benchmarks/reactivity/kairo/util.js | 6 - benchmarking/benchmarks/reactivity/sbench.js | 469 ++++++------------ .../reactivity/tests/kairo_avoidable.bench.js | 35 ++ .../reactivity/tests/kairo_broad.bench.js | 41 ++ .../reactivity/tests/kairo_deep.bench.js | 41 ++ .../reactivity/tests/kairo_diamond.bench.js | 45 ++ .../reactivity/tests/kairo_mux.bench.js | 38 ++ .../reactivity/tests/kairo_repeated.bench.js | 42 ++ .../reactivity/tests/kairo_triangle.bench.js | 55 ++ .../reactivity/tests/kairo_unstable.bench.js | 41 ++ .../{mol_bench.js => tests/mol.bench.js} | 62 +-- benchmarking/benchmarks/reactivity/util.js | 73 +++ 21 files changed, 577 insertions(+), 1203 deletions(-) delete mode 100644 benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js delete mode 100644 benchmarking/benchmarks/reactivity/kairo/kairo_broad.js delete mode 100644 benchmarking/benchmarks/reactivity/kairo/kairo_deep.js delete mode 100644 benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js delete mode 100644 benchmarking/benchmarks/reactivity/kairo/kairo_mux.js delete mode 100644 benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js delete mode 100644 benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js delete mode 100644 benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js delete mode 100644 benchmarking/benchmarks/reactivity/kairo/util.js create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js rename benchmarking/benchmarks/reactivity/{mol_bench.js => tests/mol.bench.js} (56%) create mode 100644 benchmarking/benchmarks/reactivity/util.js diff --git a/benchmarking/benchmarks/reactivity/index.js b/benchmarking/benchmarks/reactivity/index.js index 58b3f5cb29..2b75b3dfc6 100644 --- a/benchmarking/benchmarks/reactivity/index.js +++ b/benchmarking/benchmarks/reactivity/index.js @@ -1,12 +1,5 @@ -import { kairo_avoidable_owned, kairo_avoidable_unowned } from './kairo/kairo_avoidable.js'; -import { kairo_broad_owned, kairo_broad_unowned } from './kairo/kairo_broad.js'; -import { kairo_deep_owned, kairo_deep_unowned } from './kairo/kairo_deep.js'; -import { kairo_diamond_owned, kairo_diamond_unowned } from './kairo/kairo_diamond.js'; -import { kairo_mux_unowned, kairo_mux_owned } from './kairo/kairo_mux.js'; -import { kairo_repeated_unowned, kairo_repeated_owned } from './kairo/kairo_repeated.js'; -import { kairo_triangle_owned, kairo_triangle_unowned } from './kairo/kairo_triangle.js'; -import { kairo_unstable_owned, kairo_unstable_unowned } from './kairo/kairo_unstable.js'; -import { mol_bench_owned, mol_bench_unowned } from './mol_bench.js'; +import fs from 'node:fs'; +import path from 'node:path'; import { sbench_create_0to1, sbench_create_1000to1, @@ -19,10 +12,14 @@ import { sbench_create_4to1, sbench_create_signals } from './sbench.js'; +import { fileURLToPath } from 'node:url'; +import { create_test } from './util.js'; // This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark) // Not all tests are the same, and many parts have been tweaked to capture different data. +const dirname = path.dirname(fileURLToPath(import.meta.url)); + export const reactivity_benchmarks = [ sbench_create_signals, sbench_create_0to1, @@ -33,23 +30,16 @@ export const reactivity_benchmarks = [ sbench_create_1to2, sbench_create_1to4, sbench_create_1to8, - sbench_create_1to1000, - kairo_avoidable_owned, - kairo_avoidable_unowned, - kairo_broad_owned, - kairo_broad_unowned, - kairo_deep_owned, - kairo_deep_unowned, - kairo_diamond_owned, - kairo_diamond_unowned, - kairo_triangle_owned, - kairo_triangle_unowned, - kairo_mux_owned, - kairo_mux_unowned, - kairo_repeated_owned, - kairo_repeated_unowned, - kairo_unstable_owned, - kairo_unstable_unowned, - mol_bench_owned, - mol_bench_unowned + sbench_create_1to1000 ]; + +for (const file of fs.readdirSync(`${dirname}/tests`)) { + if (!file.includes('.bench.')) continue; + + const name = file.replace('.bench.js', ''); + + const module = await import(`${dirname}/tests/${file}`); + const { owned, unowned } = create_test(name, module.default); + + reactivity_benchmarks.push(owned, unowned); +} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js deleted file mode 100644 index 9daea6de99..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js +++ /dev/null @@ -1,91 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; -import { busy } from './util.js'; - -function setup() { - let head = $.state(0); - let computed1 = $.derived(() => $.get(head)); - let computed2 = $.derived(() => ($.get(computed1), 0)); - let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation - let computed4 = $.derived(() => $.get(computed3) + 2); - let computed5 = $.derived(() => $.get(computed4) + 3); - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(computed5); - busy(); // heavy side effect - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - assert($.get(computed5) === 6); - for (let i = 0; i < 1000; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(computed5) === 6); - } - } - }; -} - -export async function kairo_avoidable_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_avoidable_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_avoidable_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_avoidable_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js b/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js deleted file mode 100644 index 8dc5710c87..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js +++ /dev/null @@ -1,97 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -function setup() { - let head = $.state(0); - let last = head; - let counter = 0; - - const destroy = $.effect_root(() => { - for (let i = 0; i < 50; i++) { - let current = $.derived(() => { - return $.get(head) + i; - }); - let current2 = $.derived(() => { - return $.get(current) + 1; - }); - $.render_effect(() => { - $.get(current2); - counter++; - }); - last = current2; - } - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - counter = 0; - for (let i = 0; i < 50; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(last) === i + 50); - } - assert(counter === 50 * 50); - } - }; -} - -export async function kairo_broad_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_broad_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_broad_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_broad_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js b/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js deleted file mode 100644 index 8690c85f86..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js +++ /dev/null @@ -1,97 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -let len = 50; -const iter = 50; - -function setup() { - let head = $.state(0); - let current = head; - for (let i = 0; i < len; i++) { - let c = current; - current = $.derived(() => { - return $.get(c) + 1; - }); - } - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(current); - counter++; - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - counter = 0; - for (let i = 0; i < iter; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(current) === len + i); - } - assert(counter === iter); - } - }; -} - -export async function kairo_deep_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_deep_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_deep_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_deep_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js b/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js deleted file mode 100644 index bf4e07ee89..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js +++ /dev/null @@ -1,101 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -let width = 5; - -function setup() { - let head = $.state(0); - let current = []; - for (let i = 0; i < width; i++) { - current.push( - $.derived(() => { - return $.get(head) + 1; - }) - ); - } - let sum = $.derived(() => { - return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0); - }); - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(sum); - counter++; - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - assert($.get(sum) === 2 * width); - counter = 0; - for (let i = 0; i < 500; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(sum) === (i + 1) * width); - } - assert(counter === 500); - } - }; -} - -export async function kairo_diamond_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_diamond_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_diamond_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_diamond_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js b/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js deleted file mode 100644 index fc252a27b5..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js +++ /dev/null @@ -1,94 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -function setup() { - let heads = new Array(100).fill(null).map((_) => $.state(0)); - const mux = $.derived(() => { - return Object.fromEntries(heads.map((h) => $.get(h)).entries()); - }); - const splited = heads - .map((_, index) => $.derived(() => $.get(mux)[index])) - .map((x) => $.derived(() => $.get(x) + 1)); - - const destroy = $.effect_root(() => { - splited.forEach((x) => { - $.render_effect(() => { - $.get(x); - }); - }); - }); - - return { - destroy, - run() { - for (let i = 0; i < 10; i++) { - $.flush(() => { - $.set(heads[i], i); - }); - assert($.get(splited[i]) === i + 1); - } - for (let i = 0; i < 10; i++) { - $.flush(() => { - $.set(heads[i], i * 2); - }); - assert($.get(splited[i]) === i * 2 + 1); - } - } - }; -} - -export async function kairo_mux_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_mux_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_mux_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_mux_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js b/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js deleted file mode 100644 index 3bee06ca0e..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js +++ /dev/null @@ -1,98 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -let size = 30; - -function setup() { - let head = $.state(0); - let current = $.derived(() => { - let result = 0; - for (let i = 0; i < size; i++) { - result += $.get(head); - } - return result; - }); - - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(current); - counter++; - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - assert($.get(current) === size); - counter = 0; - for (let i = 0; i < 100; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(current) === i * size); - } - assert(counter === 100); - } - }; -} - -export async function kairo_repeated_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_repeated_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_repeated_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_repeated_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js b/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js deleted file mode 100644 index 11a419a52e..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js +++ /dev/null @@ -1,111 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -let width = 10; - -function count(number) { - return new Array(number) - .fill(0) - .map((_, i) => i + 1) - .reduce((x, y) => x + y, 0); -} - -function setup() { - let head = $.state(0); - let current = head; - let list = []; - for (let i = 0; i < width; i++) { - let c = current; - list.push(current); - current = $.derived(() => { - return $.get(c) + 1; - }); - } - let sum = $.derived(() => { - return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0); - }); - - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(sum); - counter++; - }); - }); - - return { - destroy, - run() { - const constant = count(width); - $.flush(() => { - $.set(head, 1); - }); - assert($.get(sum) === constant); - counter = 0; - for (let i = 0; i < 100; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(sum) === constant - width + i * width); - } - assert(counter === 100); - } - }; -} - -export async function kairo_triangle_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_triangle_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_triangle_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_triangle_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js deleted file mode 100644 index 54eb732cb2..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js +++ /dev/null @@ -1,97 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -function setup() { - let head = $.state(0); - const double = $.derived(() => $.get(head) * 2); - const inverse = $.derived(() => -$.get(head)); - let current = $.derived(() => { - let result = 0; - for (let i = 0; i < 20; i++) { - result += $.get(head) % 2 ? $.get(double) : $.get(inverse); - } - return result; - }); - - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(current); - counter++; - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - assert($.get(current) === 40); - counter = 0; - for (let i = 0; i < 100; i++) { - $.flush(() => { - $.set(head, i); - }); - } - assert(counter === 100); - } - }; -} - -export async function kairo_unstable_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_unstable_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_unstable_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_unstable_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/util.js b/benchmarking/benchmarks/reactivity/kairo/util.js deleted file mode 100644 index 75e3641ab9..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/util.js +++ /dev/null @@ -1,6 +0,0 @@ -export function busy() { - let a = 0; - for (let i = 0; i < 1_00; i++) { - a++; - } -} diff --git a/benchmarking/benchmarks/reactivity/sbench.js b/benchmarking/benchmarks/reactivity/sbench.js index ddeaef2514..196c29dc8d 100644 --- a/benchmarking/benchmarks/reactivity/sbench.js +++ b/benchmarking/benchmarks/reactivity/sbench.js @@ -1,3 +1,4 @@ +/** @import { Source } from '../../../packages/svelte/src/internal/client/types.js' */ import { fastest_test } from '../../utils.js'; import * as $ from '../../../packages/svelte/src/internal/client/index.js'; @@ -7,360 +8,180 @@ const COUNT = 1e5; * @param {number} n * @param {any[]} sources */ -function create_data_signals(n, sources) { +function create_sources(n, sources) { for (let i = 0; i < n; i++) { sources[i] = $.state(i); } + return sources; } /** - * @param {number} i + * @param {Source} source */ -function create_computation_0(i) { - $.derived(() => i); +function create_derived(source) { + $.derived(() => $.get(source)); } /** - * @param {any} s1 - */ -function create_computation_1(s1) { - $.derived(() => $.get(s1)); -} -/** - * @param {any} s1 - * @param {any} s2 + * + * @param {string} label + * @param {(n: number, sources: Array>)} fn + * @param {number} count + * @param {number} num_sources */ -function create_computation_2(s1, s2) { - $.derived(() => $.get(s1) + $.get(s2)); -} - -function create_computation_1000(ss, offset) { - $.derived(() => { - let sum = 0; - for (let i = 0; i < 1000; i++) { - sum += $.get(ss[offset + i]); +function create_sbench_test(label, count, num_sources, fn) { + return async () => { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + fn(count, create_sources(num_sources, [])); } - return sum; - }); -} -/** - * @param {number} n - */ -function create_computations_0to1(n) { - for (let i = 0; i < n; i++) { - create_computation_0(i); - } -} - -/** - * @param {number} n - * @param {any[]} sources - */ -function create_computations_1to1(n, sources) { - for (let i = 0; i < n; i++) { - const source = sources[i]; - create_computation_1(source); - } -} + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + fn(count, create_sources(num_sources, [])); + } + }); + destroy(); + }); -/** - * @param {number} n - * @param {any[]} sources - */ -function create_computations_2to1(n, sources) { - for (let i = 0; i < n; i++) { - create_computation_2(sources[i * 2], sources[i * 2 + 1]); - } + return { + benchmark: label, + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; + }; } -function create_computation_4(s1, s2, s3, s4) { - $.derived(() => $.get(s1) + $.get(s2) + $.get(s3) + $.get(s4)); -} +export const sbench_create_signals = create_sbench_test( + 'sbench_create_signals', + COUNT, + COUNT, + create_sources +); -function create_computations_1000to1(n, sources) { +export const sbench_create_0to1 = create_sbench_test('sbench_create_0to1', COUNT, 0, (n) => { for (let i = 0; i < n; i++) { - create_computation_1000(sources, i * 1000); - } -} - -function create_computations_1to2(n, sources) { - for (let i = 0; i < n / 2; i++) { - const source = sources[i]; - create_computation_1(source); - create_computation_1(source); - } -} - -function create_computations_1to4(n, sources) { - for (let i = 0; i < n / 4; i++) { - const source = sources[i]; - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - } -} - -function create_computations_1to8(n, sources) { - for (let i = 0; i < n / 8; i++) { - const source = sources[i]; - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); + $.derived(() => i); } -} +}); -function create_computations_1to1000(n, sources) { - for (let i = 0; i < n / 1000; i++) { - const source = sources[i]; - for (let j = 0; j < 1000; j++) { - create_computation_1(source); +export const sbench_create_1to1 = create_sbench_test( + 'sbench_create_1to1', + COUNT, + COUNT, + (n, sources) => { + for (let i = 0; i < n; i++) { + create_derived(sources[i]); } } -} - -function create_computations_4to1(n, sources) { - for (let i = 0; i < n; i++) { - create_computation_4( - sources[i * 4], - sources[i * 4 + 1], - sources[i * 4 + 2], - sources[i * 4 + 3] - ); - } -} - -/** - * @param {any} fn - * @param {number} count - * @param {number} scount - */ -function bench(fn, count, scount) { - let sources = create_data_signals(scount, []); - - fn(count, sources); -} - -export async function sbench_create_signals() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_data_signals, COUNT, COUNT); - } +); - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 100; i++) { - bench(create_data_signals, COUNT, COUNT); +export const sbench_create_2to1 = create_sbench_test( + 'sbench_create_2to1', + COUNT / 2, + COUNT, + (n, sources) => { + for (let i = 0; i < n; i++) { + $.derived(() => $.get(sources[i * 2]) + $.get(sources[i * 2 + 1])); } - }); - - return { - benchmark: 'sbench_create_signals', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_0to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_0to1, COUNT, 0); } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_0to1, COUNT, 0); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_0to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to1, COUNT, COUNT); - } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to1, COUNT, COUNT); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_2to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_2to1, COUNT / 2, COUNT); - } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_2to1, COUNT / 2, COUNT); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_2to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_4to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_4to1, COUNT / 4, COUNT); +); + +export const sbench_create_4to1 = create_sbench_test( + 'sbench_create_4to1', + COUNT / 4, + COUNT, + (n, sources) => { + for (let i = 0; i < n; i++) { + $.derived( + () => + $.get(sources[i * 4]) + + $.get(sources[i * 4 + 1]) + + $.get(sources[i * 4 + 2]) + + $.get(sources[i * 4 + 3]) + ); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_4to1, COUNT / 4, COUNT); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_4to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1000to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1000to1, COUNT / 1000, COUNT); +); + +export const sbench_create_1000to1 = create_sbench_test( + 'sbench_create_1000to1', + COUNT / 1000, + COUNT, + (n, sources) => { + for (let i = 0; i < n; i++) { + const offset = i * 1000; + + $.derived(() => { + let sum = 0; + for (let i = 0; i < 1000; i++) { + sum += $.get(sources[offset + i]); + } + return sum; + }); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1000to1, COUNT / 1000, COUNT); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1000to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to2() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to2, COUNT, COUNT / 2); +); + +export const sbench_create_1to2 = create_sbench_test( + 'sbench_create_1to2', + COUNT, + COUNT / 2, + (n, sources) => { + for (let i = 0; i < n / 2; i++) { + const source = sources[i]; + create_derived(source); + create_derived(source); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to2, COUNT, COUNT / 2); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to2', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to4() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to4, COUNT, COUNT / 4); +); + +export const sbench_create_1to4 = create_sbench_test( + 'sbench_create_1to4', + COUNT, + COUNT / 4, + (n, sources) => { + for (let i = 0; i < n / 4; i++) { + const source = sources[i]; + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to4, COUNT, COUNT / 4); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to4', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to8() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to8, COUNT, COUNT / 8); +); + +export const sbench_create_1to8 = create_sbench_test( + 'sbench_create_1to8', + COUNT, + COUNT / 8, + (n, sources) => { + for (let i = 0; i < n / 8; i++) { + const source = sources[i]; + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to8, COUNT, COUNT / 8); +); + +export const sbench_create_1to1000 = create_sbench_test( + 'sbench_create_1to1000', + COUNT, + COUNT / 1000, + (n, sources) => { + for (let i = 0; i < n / 1000; i++) { + const source = sources[i]; + for (let j = 0; j < 1000; j++) { + create_derived(source); } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to8', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to1000() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to1000, COUNT, COUNT / 1000); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to1000, COUNT, COUNT / 1000); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to1000', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} +); diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js new file mode 100644 index 0000000000..bb8c234f05 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js @@ -0,0 +1,35 @@ +import { assert } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; +import { busy } from '../util.js'; + +export default () => { + let head = $.state(0); + let computed1 = $.derived(() => $.get(head)); + let computed2 = $.derived(() => ($.get(computed1), 0)); + let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation + let computed4 = $.derived(() => $.get(computed3) + 2); + let computed5 = $.derived(() => $.get(computed4) + 3); + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(computed5); + busy(); // heavy side effect + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert($.get(computed5) === 6); + for (let i = 0; i < 1000; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(computed5) === 6); + } + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js new file mode 100644 index 0000000000..14e855566b --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js @@ -0,0 +1,41 @@ +import { assert } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +export default () => { + let head = $.state(0); + let last = head; + let counter = 0; + + const destroy = $.effect_root(() => { + for (let i = 0; i < 50; i++) { + let current = $.derived(() => { + return $.get(head) + i; + }); + let current2 = $.derived(() => { + return $.get(current) + 1; + }); + $.render_effect(() => { + $.get(current2); + counter++; + }); + last = current2; + } + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + counter = 0; + for (let i = 0; i < 50; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(last) === i + 50); + } + assert(counter === 50 * 50); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js new file mode 100644 index 0000000000..1051b0c1cf --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js @@ -0,0 +1,41 @@ +import { assert } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +let len = 50; +const iter = 50; + +export default () => { + let head = $.state(0); + let current = head; + for (let i = 0; i < len; i++) { + let c = current; + current = $.derived(() => { + return $.get(c) + 1; + }); + } + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + counter = 0; + for (let i = 0; i < iter; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(current) === len + i); + } + assert(counter === iter); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js new file mode 100644 index 0000000000..cde53fa3eb --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js @@ -0,0 +1,45 @@ +import { assert } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +let width = 5; + +export default () => { + let head = $.state(0); + let current = []; + for (let i = 0; i < width; i++) { + current.push( + $.derived(() => { + return $.get(head) + 1; + }) + ); + } + let sum = $.derived(() => { + return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0); + }); + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(sum); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert($.get(sum) === 2 * width); + counter = 0; + for (let i = 0; i < 500; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(sum) === (i + 1) * width); + } + assert(counter === 500); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js new file mode 100644 index 0000000000..cfc7592c15 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js @@ -0,0 +1,38 @@ +import { assert } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +export default () => { + let heads = new Array(100).fill(null).map((_) => $.state(0)); + const mux = $.derived(() => { + return Object.fromEntries(heads.map((h) => $.get(h)).entries()); + }); + const splited = heads + .map((_, index) => $.derived(() => $.get(mux)[index])) + .map((x) => $.derived(() => $.get(x) + 1)); + + const destroy = $.effect_root(() => { + splited.forEach((x) => { + $.render_effect(() => { + $.get(x); + }); + }); + }); + + return { + destroy, + run() { + for (let i = 0; i < 10; i++) { + $.flush(() => { + $.set(heads[i], i); + }); + assert($.get(splited[i]) === i + 1); + } + for (let i = 0; i < 10; i++) { + $.flush(() => { + $.set(heads[i], i * 2); + }); + assert($.get(splited[i]) === i * 2 + 1); + } + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js new file mode 100644 index 0000000000..2887a39931 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js @@ -0,0 +1,42 @@ +import { assert } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +let size = 30; + +export default () => { + let head = $.state(0); + let current = $.derived(() => { + let result = 0; + for (let i = 0; i < size; i++) { + result += $.get(head); + } + return result; + }); + + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert($.get(current) === size); + counter = 0; + for (let i = 0; i < 100; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(current) === i * size); + } + assert(counter === 100); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js new file mode 100644 index 0000000000..0abdd1e13d --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js @@ -0,0 +1,55 @@ +import { assert } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +let width = 10; + +function count(number) { + return new Array(number) + .fill(0) + .map((_, i) => i + 1) + .reduce((x, y) => x + y, 0); +} + +export default () => { + let head = $.state(0); + let current = head; + let list = []; + for (let i = 0; i < width; i++) { + let c = current; + list.push(current); + current = $.derived(() => { + return $.get(c) + 1; + }); + } + let sum = $.derived(() => { + return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0); + }); + + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(sum); + counter++; + }); + }); + + return { + destroy, + run() { + const constant = count(width); + $.flush(() => { + $.set(head, 1); + }); + assert($.get(sum) === constant); + counter = 0; + for (let i = 0; i < 100; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(sum) === constant - width + i * width); + } + assert(counter === 100); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js new file mode 100644 index 0000000000..b09467326f --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js @@ -0,0 +1,41 @@ +import { assert } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +export default () => { + let head = $.state(0); + const double = $.derived(() => $.get(head) * 2); + const inverse = $.derived(() => -$.get(head)); + let current = $.derived(() => { + let result = 0; + for (let i = 0; i < 20; i++) { + result += $.get(head) % 2 ? $.get(double) : $.get(inverse); + } + return result; + }); + + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert($.get(current) === 40); + counter = 0; + for (let i = 0; i < 100; i++) { + $.flush(() => { + $.set(head, i); + }); + } + assert(counter === 100); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/mol_bench.js b/benchmarking/benchmarks/reactivity/tests/mol.bench.js similarity index 56% rename from benchmarking/benchmarks/reactivity/mol_bench.js rename to benchmarking/benchmarks/reactivity/tests/mol.bench.js index 536b078d74..56b6dc8b28 100644 --- a/benchmarking/benchmarks/reactivity/mol_bench.js +++ b/benchmarking/benchmarks/reactivity/tests/mol.bench.js @@ -1,4 +1,4 @@ -import { assert, fastest_test } from '../../utils.js'; +import { assert } from '../../../utils.js'; import * as $ from 'svelte/internal/client'; /** @@ -18,7 +18,7 @@ function hard(n) { const numbers = Array.from({ length: 5 }, (_, i) => i); -function setup() { +export default () => { let res = []; const A = $.state(0); const B = $.state(0); @@ -62,60 +62,4 @@ function setup() { assert(res[0] === 3198 && res[1] === 1601 && res[2] === 3195 && res[3] === 1598); } }; -} - -export async function mol_bench_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(0); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1e4; i++) { - run(i); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'mol_bench_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function mol_bench_unowned() { - // 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_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} +}; diff --git a/benchmarking/benchmarks/reactivity/util.js b/benchmarking/benchmarks/reactivity/util.js new file mode 100644 index 0000000000..18c4f556af --- /dev/null +++ b/benchmarking/benchmarks/reactivity/util.js @@ -0,0 +1,73 @@ +import * as $ from 'svelte/internal/client'; +import { fastest_test } from '../../utils.js'; + +export function busy() { + let a = 0; + for (let i = 0; i < 1_00; i++) { + a++; + } +} + +/** + * + * @param {string} label + * @param {() => { run: (i?: number) => void, destroy: () => void }} setup + */ +export function create_test(label, setup) { + return { + unowned: async () => { + // 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 < 1000; i++) { + run(i); + } + }); + + destroy(); + + return { + benchmark: `${label}_unowned`, + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; + }, + owned: async () => { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(0); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(i); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: `${label}_owned`, + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; + } + }; +} From 46c810ce71d14e165cab23fb5556e0ebfe3abf7d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 21 Jan 2026 13:57:18 -0500 Subject: [PATCH 3/7] chore: tidy up the benchmarking code (#17504) * chore: tidy up the benchmarking code * use built-in assert * fix find-replace fail --- benchmarking/benchmarks/reactivity/sbench.js | 6 +- .../reactivity/tests/kairo_avoidable.bench.js | 6 +- .../reactivity/tests/kairo_broad.bench.js | 6 +- .../reactivity/tests/kairo_deep.bench.js | 6 +- .../reactivity/tests/kairo_diamond.bench.js | 8 +- .../reactivity/tests/kairo_mux.bench.js | 6 +- .../reactivity/tests/kairo_repeated.bench.js | 8 +- .../reactivity/tests/kairo_triangle.bench.js | 8 +- .../reactivity/tests/kairo_unstable.bench.js | 6 +- .../benchmarks/reactivity/tests/mol.bench.js | 7 +- benchmarking/benchmarks/reactivity/util.js | 12 +- .../benchmarks/ssr/wrapper/wrapper_bench.js | 33 ++++- benchmarking/utils.js | 114 +++--------------- 13 files changed, 88 insertions(+), 138 deletions(-) diff --git a/benchmarking/benchmarks/reactivity/sbench.js b/benchmarking/benchmarks/reactivity/sbench.js index 196c29dc8d..47ff4a621a 100644 --- a/benchmarking/benchmarks/reactivity/sbench.js +++ b/benchmarking/benchmarks/reactivity/sbench.js @@ -37,7 +37,7 @@ function create_sbench_test(label, count, num_sources, fn) { fn(count, create_sources(num_sources, [])); } - const { timing } = await fastest_test(10, () => { + const { time, gc_time } = await fastest_test(10, () => { const destroy = $.effect_root(() => { for (let i = 0; i < 10; i++) { fn(count, create_sources(num_sources, [])); @@ -48,8 +48,8 @@ function create_sbench_test(label, count, num_sources, fn) { return { benchmark: label, - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) + time: time.toFixed(2), + gc_time: gc_time.toFixed(2) }; }; } diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js index bb8c234f05..d4ba858824 100644 --- a/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js +++ b/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js @@ -1,4 +1,4 @@ -import { assert } from '../../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; import { busy } from '../util.js'; @@ -23,12 +23,12 @@ export default () => { $.flush(() => { $.set(head, 1); }); - assert($.get(computed5) === 6); + assert.equal($.get(computed5), 6); for (let i = 0; i < 1000; i++) { $.flush(() => { $.set(head, i); }); - assert($.get(computed5) === 6); + assert.equal($.get(computed5), 6); } } }; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js index 14e855566b..aebae7a898 100644 --- a/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js +++ b/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js @@ -1,4 +1,4 @@ -import { assert } from '../../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; export default () => { @@ -33,9 +33,9 @@ export default () => { $.flush(() => { $.set(head, i); }); - assert($.get(last) === i + 50); + assert.equal($.get(last), i + 50); } - assert(counter === 50 * 50); + assert.equal(counter, 50 * 50); } }; }; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js index 1051b0c1cf..4a361e9bfc 100644 --- a/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js +++ b/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js @@ -1,4 +1,4 @@ -import { assert } from '../../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; let len = 50; @@ -33,9 +33,9 @@ export default () => { $.flush(() => { $.set(head, i); }); - assert($.get(current) === len + i); + assert.equal($.get(current), len + i); } - assert(counter === iter); + assert.equal(counter, iter); } }; }; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js index cde53fa3eb..17d9bd85e5 100644 --- a/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js +++ b/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js @@ -1,4 +1,4 @@ -import { assert } from '../../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; let width = 5; @@ -31,15 +31,15 @@ export default () => { $.flush(() => { $.set(head, 1); }); - assert($.get(sum) === 2 * width); + assert.equal($.get(sum), 2 * width); counter = 0; for (let i = 0; i < 500; i++) { $.flush(() => { $.set(head, i); }); - assert($.get(sum) === (i + 1) * width); + assert.equal($.get(sum), (i + 1) * width); } - assert(counter === 500); + assert.equal(counter, 500); } }; }; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js index cfc7592c15..4af6bf7873 100644 --- a/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js +++ b/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js @@ -1,4 +1,4 @@ -import { assert } from '../../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; export default () => { @@ -25,13 +25,13 @@ export default () => { $.flush(() => { $.set(heads[i], i); }); - assert($.get(splited[i]) === i + 1); + assert.equal($.get(splited[i]), i + 1); } for (let i = 0; i < 10; i++) { $.flush(() => { $.set(heads[i], i * 2); }); - assert($.get(splited[i]) === i * 2 + 1); + assert.equal($.get(splited[i]), i * 2 + 1); } } }; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js index 2887a39931..cab7689fea 100644 --- a/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js +++ b/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js @@ -1,4 +1,4 @@ -import { assert } from '../../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; let size = 30; @@ -28,15 +28,15 @@ export default () => { $.flush(() => { $.set(head, 1); }); - assert($.get(current) === size); + assert.equal($.get(current), size); counter = 0; for (let i = 0; i < 100; i++) { $.flush(() => { $.set(head, i); }); - assert($.get(current) === i * size); + assert.equal($.get(current), i * size); } - assert(counter === 100); + assert.equal(counter, 100); } }; }; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js index 0abdd1e13d..b4b46c0209 100644 --- a/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js +++ b/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js @@ -1,4 +1,4 @@ -import { assert } from '../../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; let width = 10; @@ -41,15 +41,15 @@ export default () => { $.flush(() => { $.set(head, 1); }); - assert($.get(sum) === constant); + assert.equal($.get(sum), constant); counter = 0; for (let i = 0; i < 100; i++) { $.flush(() => { $.set(head, i); }); - assert($.get(sum) === constant - width + i * width); + assert.equal($.get(sum), constant - width + i * width); } - assert(counter === 100); + assert.equal(counter, 100); } }; }; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js index b09467326f..e7723fae0d 100644 --- a/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js +++ b/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js @@ -1,4 +1,4 @@ -import { assert } from '../../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; export default () => { @@ -28,14 +28,14 @@ export default () => { $.flush(() => { $.set(head, 1); }); - assert($.get(current) === 40); + assert.equal($.get(current), 40); counter = 0; for (let i = 0; i < 100; i++) { $.flush(() => { $.set(head, i); }); } - assert(counter === 100); + assert.equal(counter, 100); } }; }; diff --git a/benchmarking/benchmarks/reactivity/tests/mol.bench.js b/benchmarking/benchmarks/reactivity/tests/mol.bench.js index 56b6dc8b28..e66f0191d1 100644 --- a/benchmarking/benchmarks/reactivity/tests/mol.bench.js +++ b/benchmarking/benchmarks/reactivity/tests/mol.bench.js @@ -1,4 +1,4 @@ -import { assert } from '../../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; /** @@ -59,7 +59,10 @@ export default () => { $.set(A, 2 + i * 2); $.set(B, 2); }); - assert(res[0] === 3198 && res[1] === 1601 && res[2] === 3195 && res[3] === 1598); + assert.equal(res[0], 3198); + assert.equal(res[1], 1601); + assert.equal(res[2], 3195); + assert.equal(res[3], 1598); } }; }; diff --git a/benchmarking/benchmarks/reactivity/util.js b/benchmarking/benchmarks/reactivity/util.js index 18c4f556af..ff0722d85e 100644 --- a/benchmarking/benchmarks/reactivity/util.js +++ b/benchmarking/benchmarks/reactivity/util.js @@ -25,7 +25,7 @@ export function create_test(label, setup) { const { run, destroy } = setup(); - const { timing } = await fastest_test(10, () => { + const { time, gc_time } = await fastest_test(10, () => { for (let i = 0; i < 1000; i++) { run(i); } @@ -35,8 +35,8 @@ export function create_test(label, setup) { return { benchmark: `${label}_unowned`, - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) + time: time.toFixed(2), + gc_time: gc_time.toFixed(2) }; }, owned: async () => { @@ -53,7 +53,7 @@ export function create_test(label, setup) { ({ run, destroy } = setup()); }); - const { timing } = await fastest_test(10, () => { + const { time, gc_time } = await fastest_test(10, () => { for (let i = 0; i < 1000; i++) { run(i); } @@ -65,8 +65,8 @@ export function create_test(label, setup) { return { benchmark: `${label}_owned`, - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) + time: time.toFixed(2), + gc_time: gc_time.toFixed(2) }; } }; diff --git a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js index ba0457b80e..6fc992d4a2 100644 --- a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js +++ b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js @@ -1,13 +1,16 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { render } from 'svelte/server'; -import { fastest_test, read_file, write } from '../../../utils.js'; +import { fastest_test } from '../../../utils.js'; import { compile } from 'svelte/compiler'; const dir = `${process.cwd()}/benchmarking/benchmarks/ssr/wrapper`; async function compile_svelte() { - const output = compile(read_file(`${dir}/App.svelte`), { + const output = compile(read(`${dir}/App.svelte`), { generate: 'server' }); + write(`${dir}/output/App.js`, output.js.code); const module = await import(`${dir}/output/App.js`); @@ -17,12 +20,13 @@ async function compile_svelte() { export async function wrapper_bench() { const App = await compile_svelte(); + // Do 3 loops to warm up JIT for (let i = 0; i < 3; i++) { render(App); } - const { timing } = await fastest_test(10, () => { + const { time, gc_time } = await fastest_test(10, () => { for (let i = 0; i < 100; i++) { render(App); } @@ -30,7 +34,26 @@ export async function wrapper_bench() { return { benchmark: 'wrapper_bench', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) + time: time.toFixed(2), + gc_time: gc_time.toFixed(2) }; } + +/** + * @param {string} file + */ +function read(file) { + return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n'); +} + +/** + * @param {string} file + * @param {string} contents + */ +function write(file, contents) { + try { + fs.mkdirSync(path.dirname(file), { recursive: true }); + } catch {} + + fs.writeFileSync(file, contents); +} diff --git a/benchmarking/utils.js b/benchmarking/utils.js index 684d2ee02b..5581135e00 100644 --- a/benchmarking/utils.js +++ b/benchmarking/utils.js @@ -1,78 +1,30 @@ import { performance, PerformanceObserver } from 'node:perf_hooks'; import v8 from 'v8-natives'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; // 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 = []; +async function track(fn) { + v8.collectGarbage(); - 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 }); + /** @type {PerformanceEntry[]} */ + const entries = []; - return { result, track_id: this.track_id }; - } + const observer = new PerformanceObserver((list) => entries.push(...list.getEntries())); + observer.observe({ entryTypes: ['gc'] }); - /** - * @param {number} track_id - */ - async gcDuration(track_id) { - await promise_delay(10); + const start = performance.now(); + fn(); + const end = performance.now(); - 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'); - } + await new Promise((f) => setTimeout(f, 10)); - const entries = this.perf_entries.filter( - (e) => e.startTime >= period.start && e.startTime < period.end - ); - return entries.reduce((t, e) => e.duration + t, 0); - } + const gc_time = entries + .filter((e) => e.startTime >= start && e.startTime < end) + .reduce((t, e) => e.duration + t, 0); - destroy() { - this.observer.disconnect(); - } + 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 } }; + return { time: end - start, gc_time }; } /** @@ -80,40 +32,12 @@ async function run_tracked(fn) { * @param {() => void} fn */ export async function fastest_test(times, fn) { + /** @type {Array<{ time: number, gc_time: number }>} */ 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; -} - -/** - * @param {boolean} a - */ -export function assert(a) { - if (!a) { - throw new Error('Assertion failed'); + for (let i = 0; i < times; i++) { + results.push(await track(fn)); } -} - -/** - * @param {string} file - */ -export function read_file(file) { - return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n'); -} - -/** - * @param {string} file - * @param {string} contents - */ -export function write(file, contents) { - try { - fs.mkdirSync(path.dirname(file), { recursive: true }); - } catch {} - fs.writeFileSync(file, contents); + return results.reduce((a, b) => (a.time < b.time ? a : b)); } From c41fef1ef104f4797cca45051774d4a662156f9a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 21 Jan 2026 14:45:07 -0500 Subject: [PATCH 4/7] Nicer bench output (#17505) * test * dont coerce to string * more readable output for pnpm bench * oops * fix --- benchmarking/benchmarks/reactivity/sbench.js | 4 +- benchmarking/benchmarks/reactivity/util.js | 8 +-- .../benchmarks/ssr/wrapper/wrapper_bench.js | 4 +- benchmarking/run.js | 64 +++++++++++-------- package.json | 2 +- 5 files changed, 48 insertions(+), 34 deletions(-) diff --git a/benchmarking/benchmarks/reactivity/sbench.js b/benchmarking/benchmarks/reactivity/sbench.js index 47ff4a621a..9c41f60746 100644 --- a/benchmarking/benchmarks/reactivity/sbench.js +++ b/benchmarking/benchmarks/reactivity/sbench.js @@ -48,8 +48,8 @@ function create_sbench_test(label, count, num_sources, fn) { return { benchmark: label, - time: time.toFixed(2), - gc_time: gc_time.toFixed(2) + time, + gc_time }; }; } diff --git a/benchmarking/benchmarks/reactivity/util.js b/benchmarking/benchmarks/reactivity/util.js index ff0722d85e..6a2e44f5ae 100644 --- a/benchmarking/benchmarks/reactivity/util.js +++ b/benchmarking/benchmarks/reactivity/util.js @@ -35,8 +35,8 @@ export function create_test(label, setup) { return { benchmark: `${label}_unowned`, - time: time.toFixed(2), - gc_time: gc_time.toFixed(2) + time, + gc_time }; }, owned: async () => { @@ -65,8 +65,8 @@ export function create_test(label, setup) { return { benchmark: `${label}_owned`, - time: time.toFixed(2), - gc_time: gc_time.toFixed(2) + time, + gc_time }; } }; diff --git a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js index 6fc992d4a2..6e790ee2b8 100644 --- a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js +++ b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js @@ -34,8 +34,8 @@ export async function wrapper_bench() { return { benchmark: 'wrapper_bench', - time: time.toFixed(2), - gc_time: gc_time.toFixed(2) + time, + gc_time }; } diff --git a/benchmarking/run.js b/benchmarking/run.js index bd96b9c2dc..f7b8128886 100644 --- a/benchmarking/run.js +++ b/benchmarking/run.js @@ -10,46 +10,60 @@ const suites = [ { benchmarks: ssr_benchmarks, name: 'server-side rendering benchmarks' } ]; -// eslint-disable-next-line no-console -console.log('\x1b[1m', '-- Benchmarking Started --', '\x1b[0m'); +const COLUMN_WIDTHS = [25, 9, 9]; +const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b); + +const pad_right = (str, n) => str + ' '.repeat(n - str.length); +const pad_left = (str, n) => ' '.repeat(n - str.length) + str; + $.push({}, true); + try { for (const { benchmarks, name } of suites) { let suite_time = 0; let suite_gc_time = 0; - // eslint-disable-next-line no-console + console.log(`\nRunning ${name}...\n`); + console.log( + pad_right('Benchmark', COLUMN_WIDTHS[0]) + + pad_left('Time', COLUMN_WIDTHS[1]) + + pad_left('GC time', COLUMN_WIDTHS[2]) + ); + console.log('='.repeat(TOTAL_WIDTH)); for (const benchmark of benchmarks) { const results = await benchmark(); - // eslint-disable-next-line no-console - console.log(results); - total_time += Number(results.time); - total_gc_time += Number(results.gc_time); - suite_time += Number(results.time); - suite_gc_time += Number(results.gc_time); + console.log( + pad_right(results.benchmark, COLUMN_WIDTHS[0]) + + pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) + + pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2]) + ); + total_time += results.time; + total_gc_time += results.gc_time; + suite_time += results.time; + suite_gc_time += results.gc_time; } - console.log(`\nFinished ${name}.\n`); - - // eslint-disable-next-line no-console - console.log({ - suite_time: suite_time.toFixed(2), - suite_gc_time: suite_gc_time.toFixed(2) - }); + console.log('='.repeat(TOTAL_WIDTH)); + console.log( + pad_right('suite', COLUMN_WIDTHS[0]) + + pad_left(suite_time.toFixed(2), COLUMN_WIDTHS[1]) + + pad_left(suite_gc_time.toFixed(2), COLUMN_WIDTHS[2]) + ); + console.log('='.repeat(TOTAL_WIDTH)); } } catch (e) { - // eslint-disable-next-line no-console - console.log('\x1b[1m', '\n-- Benchmarking Failed --\n', '\x1b[0m'); // eslint-disable-next-line no-console console.error(e); process.exit(1); } + $.pop(); -// eslint-disable-next-line no-console -console.log('\x1b[1m', '\n-- Benchmarking Complete --\n', '\x1b[0m'); -// eslint-disable-next-line no-console -console.log({ - total_time: total_time.toFixed(2), - total_gc_time: total_gc_time.toFixed(2) -}); + +console.log(''); + +console.log( + pad_right('total', COLUMN_WIDTHS[0]) + + pad_left(total_time.toFixed(2), COLUMN_WIDTHS[1]) + + pad_left(total_gc_time.toFixed(2), COLUMN_WIDTHS[2]) +); diff --git a/package.json b/package.json index 12e59a2665..aa9c8cafa3 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test": "vitest run", "changeset:version": "changeset version && pnpm -r generate:version && git add --all", "changeset:publish": "changeset publish", - "bench": "node --allow-natives-syntax ./benchmarking/run.js", + "bench": "NODE_ENV=production node --allow-natives-syntax ./benchmarking/run.js", "bench:compare": "node --allow-natives-syntax ./benchmarking/compare/index.js", "bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js" }, From 0025d42366fa3652216cac8bf3adc1389ae49a3d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 21 Jan 2026 15:53:37 -0500 Subject: [PATCH 5/7] chore: skippable benchmarks (#17506) * chore: add benchmark filtering mechanism * tweak * fix * try this * better logging for bench:compare * fix --- benchmarking/benchmarks/reactivity/sbench.js | 35 ++++---- benchmarking/benchmarks/reactivity/util.js | 84 +++++++++---------- .../benchmarks/ssr/wrapper/wrapper_bench.js | 29 +++---- benchmarking/compare/runner.js | 11 ++- benchmarking/run.js | 32 +++++-- package.json | 4 +- 6 files changed, 104 insertions(+), 91 deletions(-) diff --git a/benchmarking/benchmarks/reactivity/sbench.js b/benchmarking/benchmarks/reactivity/sbench.js index 9c41f60746..e197f970c8 100644 --- a/benchmarking/benchmarks/reactivity/sbench.js +++ b/benchmarking/benchmarks/reactivity/sbench.js @@ -26,31 +26,28 @@ function create_derived(source) { /** * * @param {string} label - * @param {(n: number, sources: Array>)} fn + * @param {(n: number, sources: Array>) => void} fn * @param {number} count * @param {number} num_sources */ function create_sbench_test(label, count, num_sources, fn) { - return async () => { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - fn(count, create_sources(num_sources, [])); - } + return { + label, + fn: async () => { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + fn(count, create_sources(num_sources, [])); + } - const { time, gc_time } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - fn(count, create_sources(num_sources, [])); - } + return await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + fn(count, create_sources(num_sources, [])); + } + }); + destroy(); }); - destroy(); - }); - - return { - benchmark: label, - time, - gc_time - }; + } }; } diff --git a/benchmarking/benchmarks/reactivity/util.js b/benchmarking/benchmarks/reactivity/util.js index 6a2e44f5ae..da5e5c51f5 100644 --- a/benchmarking/benchmarks/reactivity/util.js +++ b/benchmarking/benchmarks/reactivity/util.js @@ -15,34 +15,9 @@ export function busy() { */ export function create_test(label, setup) { return { - unowned: async () => { - // 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 { time, gc_time } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(i); - } - }); - - destroy(); - - return { - benchmark: `${label}_unowned`, - time, - gc_time - }; - }, - owned: async () => { - let run, destroy; - - const destroy_owned = $.effect_root(() => { + unowned: { + label: `${label}_unowned`, + fn: async () => { // Do 10 loops to warm up JIT for (let i = 0; i < 10; i++) { const { run, destroy } = setup(); @@ -50,24 +25,47 @@ export function create_test(label, setup) { destroy(); } - ({ run, destroy } = setup()); - }); + const { run, destroy } = setup(); + + const result = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(i); + } + }); - const { time, gc_time } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(i); - } - }); + destroy(); + + return result; + } + }, + owned: { + label: `${label}_owned`, + fn: async () => { + let run, destroy; - // @ts-ignore - destroy(); - destroy_owned(); + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(0); + destroy(); + } - return { - benchmark: `${label}_owned`, - time, - gc_time - }; + ({ run, destroy } = setup()); + }); + + const result = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(i); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return result; + } } }; } diff --git a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js index 6e790ee2b8..9a8dda617d 100644 --- a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js +++ b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js @@ -18,26 +18,23 @@ async function compile_svelte() { return module.default; } -export async function wrapper_bench() { - const App = await compile_svelte(); +export const wrapper_bench = { + label: 'wrapper_bench', + fn: async () => { + const App = await compile_svelte(); - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - render(App); - } - - const { time, gc_time } = await fastest_test(10, () => { - for (let i = 0; i < 100; i++) { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { render(App); } - }); - return { - benchmark: 'wrapper_bench', - time, - gc_time - }; -} + return await fastest_test(10, () => { + for (let i = 0; i < 100; i++) { + render(App); + } + }); + } +}; /** * @param {string} file diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js index a2e8646379..11e40ed983 100644 --- a/benchmarking/compare/runner.js +++ b/benchmarking/compare/runner.js @@ -1,10 +1,13 @@ import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js'; const results = []; -for (const benchmark of reactivity_benchmarks) { - const result = await benchmark(); - console.error(result.benchmark); - results.push(result); + +for (let i = 0; i < reactivity_benchmarks.length; i += 1) { + const benchmark = reactivity_benchmarks[i]; + + process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `); + results.push({ benchmark: benchmark.label, ...(await benchmark.fn()) }); + process.stderr.write('\x1b[2K\r'); } process.send(results); diff --git a/benchmarking/run.js b/benchmarking/run.js index f7b8128886..2b09f7c592 100644 --- a/benchmarking/run.js +++ b/benchmarking/run.js @@ -2,13 +2,28 @@ import * as $ from '../packages/svelte/src/internal/client/index.js'; import { reactivity_benchmarks } from './benchmarks/reactivity/index.js'; import { ssr_benchmarks } from './benchmarks/ssr/index.js'; -let total_time = 0; -let total_gc_time = 0; +// e.g. `pnpm bench kairo` to only run the kairo benchmarks +const filters = process.argv.slice(2); const suites = [ - { benchmarks: reactivity_benchmarks, name: 'reactivity benchmarks' }, - { benchmarks: ssr_benchmarks, name: 'server-side rendering benchmarks' } -]; + { + benchmarks: reactivity_benchmarks.filter( + (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) + ), + name: 'reactivity benchmarks' + }, + { + benchmarks: ssr_benchmarks.filter( + (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) + ), + name: 'server-side rendering benchmarks' + } +].filter((suite) => suite.benchmarks.length > 0); + +if (suites.length === 0) { + console.log('No benchmarks matched provided filters'); + process.exit(1); +} const COLUMN_WIDTHS = [25, 9, 9]; const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b); @@ -16,6 +31,9 @@ const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b); const pad_right = (str, n) => str + ' '.repeat(n - str.length); const pad_left = (str, n) => ' '.repeat(n - str.length) + str; +let total_time = 0; +let total_gc_time = 0; + $.push({}, true); try { @@ -32,9 +50,9 @@ try { console.log('='.repeat(TOTAL_WIDTH)); for (const benchmark of benchmarks) { - const results = await benchmark(); + const results = await benchmark.fn(); console.log( - pad_right(results.benchmark, COLUMN_WIDTHS[0]) + + pad_right(benchmark.label, COLUMN_WIDTHS[0]) + pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) + pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2]) ); diff --git a/package.json b/package.json index aa9c8cafa3..24be8bd2bc 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "changeset:version": "changeset version && pnpm -r generate:version && git add --all", "changeset:publish": "changeset publish", "bench": "NODE_ENV=production node --allow-natives-syntax ./benchmarking/run.js", - "bench:compare": "node --allow-natives-syntax ./benchmarking/compare/index.js", - "bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js" + "bench:compare": "NODE_ENV=production node --allow-natives-syntax ./benchmarking/compare/index.js", + "bench:debug": "NODE_ENV=production node --allow-natives-syntax --inspect-brk ./benchmarking/run.js" }, "devDependencies": { "@changesets/cli": "^2.29.8", From 5ce61868893b5d78cc139fbd76be3ef33f4ec124 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 21 Jan 2026 16:05:37 -0500 Subject: [PATCH 6/7] fix (#17507) --- .../reactivity/tests/repeated_deps.bench.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 benchmarking/benchmarks/reactivity/tests/repeated_deps.bench.js diff --git a/benchmarking/benchmarks/reactivity/tests/repeated_deps.bench.js b/benchmarking/benchmarks/reactivity/tests/repeated_deps.bench.js new file mode 100644 index 0000000000..a8fbcfdbd6 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/repeated_deps.bench.js @@ -0,0 +1,35 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; + +const ARRAY_SIZE = 1000; + +export default () => { + const signals = Array.from({ length: ARRAY_SIZE }, (_, i) => $.state(i)); + const order = $.state(0); + + // break skipped_deps fast path by changing order of reads + const total = $.derived(() => { + const ord = $.get(order); + let sum = 0; + for (let i = 0; i < ARRAY_SIZE; i++) { + sum += /** @type {number} */ ($.get(signals[(i + ord) % ARRAY_SIZE])); + } + return sum; + }); + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(total); + }); + }); + + return { + destroy, + run() { + for (let i = 0; i < 5; i++) { + $.flush(() => $.set(order, i)); + assert.equal($.get(total), (ARRAY_SIZE * (ARRAY_SIZE - 1)) / 2); // sum of 0..999 + } + } + }; +}; From c9ebd6a88553a3041d68c6063ec095fdd601f4b7 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Wed, 21 Jan 2026 14:09:30 -0700 Subject: [PATCH 7/7] feat: export `parseCss` from `svelte/compiler` (#17496) * feat: export from * changeset * fix: Errors * set_source, so state is correct if an error occurs * simplify --------- Co-authored-by: Rich Harris --- .changeset/loud-buses-guess.md | 5 + packages/svelte/src/compiler/index.js | 26 +++- .../src/compiler/phases/1-parse/index.js | 14 ++ .../src/compiler/phases/1-parse/read/style.js | 30 ++-- packages/svelte/tests/css-parse.test.ts | 138 ++++++++++++++++++ packages/svelte/types/index.d.ts | 6 + 6 files changed, 205 insertions(+), 14 deletions(-) create mode 100644 .changeset/loud-buses-guess.md create mode 100644 packages/svelte/tests/css-parse.test.ts diff --git a/.changeset/loud-buses-guess.md b/.changeset/loud-buses-guess.md new file mode 100644 index 0000000000..d6c7c3d5a4 --- /dev/null +++ b/.changeset/loud-buses-guess.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: export `parseCss` from `svelte/compiler` diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 9095a2a2f2..acfef6a320 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -3,8 +3,9 @@ /** @import { AST } from './public.js' */ import { walk as zimmerframe_walk } from 'zimmerframe'; import { convert } from './legacy.js'; -import { parse as _parse } from './phases/1-parse/index.js'; +import { parse as _parse, Parser } from './phases/1-parse/index.js'; import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js'; +import { parse_stylesheet } from './phases/1-parse/read/style.js'; import { analyze_component, analyze_module } from './phases/2-analyze/index.js'; import { transform_component, transform_module } from './phases/3-transform/index.js'; import { validate_component_options, validate_module_options } from './validate-options.js'; @@ -118,6 +119,29 @@ export function parse(source, { modern, loose } = {}) { return to_public_ast(source, ast, modern); } +/** + * The parseCss function parses a CSS stylesheet, returning its abstract syntax tree. + * + * @param {string} source The CSS source code + * @returns {Omit} + */ +export function parseCss(source) { + source = remove_bom(source); + state.reset({ warning: () => false, filename: undefined }); + + state.set_source(source); + + const parser = Parser.forCss(source); + const children = parse_stylesheet(parser); + + return { + type: 'StyleSheet', + start: 0, + end: source.length, + children + }; +} + /** * @param {string} source * @param {AST.Root} ast diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 3b5f83d58f..88b4352e8a 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -34,6 +34,20 @@ export class Parser { /** */ index = 0; + /** + * Creates a minimal parser instance for CSS-only parsing. + * Skips Svelte component parsing setup. + * @param {string} source + * @returns {Parser} + */ + static forCss(source) { + const parser = Object.create(Parser.prototype); + parser.template = source; + parser.index = 0; + parser.loose = false; + return parser; + } + /** Whether we're parsing in TypeScript mode */ ts = false; diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 80ab234d92..c88a3707c1 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -24,10 +24,11 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/; */ export default function read_style(parser, start, attributes) { const content_start = parser.index; - const children = read_body(parser, ' p.match('= p.template.length); const content_end = parser.index; - parser.read(/^<\/style\s*>/); + parser.eat('/); return { type: 'StyleSheet', @@ -46,20 +47,14 @@ export default function read_style(parser, start, attributes) { /** * @param {Parser} parser - * @param {string} close - * @returns {any[]} + * @param {(parser: Parser) => boolean} finished + * @returns {Array} */ -function read_body(parser, close) { +function read_body(parser, finished) { /** @type {Array} */ const children = []; - while (parser.index < parser.template.length) { - allow_comment_or_whitespace(parser); - - if (parser.match(close)) { - return children; - } - + while ((allow_comment_or_whitespace(parser), !finished(parser))) { if (parser.match('@')) { children.push(read_at_rule(parser)); } else { @@ -67,7 +62,7 @@ function read_body(parser, close) { } } - e.expected_token(parser.template.length, close); + return children; } /** @@ -627,3 +622,12 @@ function allow_comment_or_whitespace(parser) { parser.allow_whitespace(); } } + +/** + * Parse standalone CSS content (not wrapped in `