Merge branch 'main' into dependencies-set

dependencies-set
Rich Harris 1 week ago
commit ae49d47e52

@ -26,31 +26,28 @@ function create_derived(source) {
/**
*
* @param {string} label
* @param {(n: number, sources: Array<Source<number>>)} fn
* @param {(n: number, sources: Array<Source<number>>) => 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 { timing } = 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: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
};
}

@ -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);
}
}
};

@ -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);
}
};
};

@ -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);
}
};
};

@ -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);
}
};
};

@ -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);
}
}
};

@ -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);
}
};
};

@ -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);
}
};
};

@ -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);
}
};
};

@ -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);
}
};
};

@ -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
}
}
};
};

@ -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 { 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(() => {
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 { timing } = 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: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
({ 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;
}
}
};
}

@ -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`);
@ -15,22 +18,39 @@ async function compile_svelte() {
return module.default;
}
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);
}
export const wrapper_bench = {
label: 'wrapper_bench',
fn: async () => {
const App = await compile_svelte();
const { timing } = 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: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
return await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
render(App);
}
});
}
};
/**
* @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);
}

@ -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);

@ -2,54 +2,86 @@ 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);
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;
// eslint-disable-next-line no-console
console.log('\x1b[1m', '-- Benchmarking Started --', '\x1b[0m');
$.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);
const results = await benchmark.fn();
console.log(
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])
);
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])
);

@ -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));
}

@ -21,9 +21,9 @@
"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:compare": "node --allow-natives-syntax ./benchmarking/compare/index.js",
"bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
"bench": "NODE_ENV=production node --allow-natives-syntax ./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",

Loading…
Cancel
Save