Merge branch 'main' into pr/17461

object-blockers
Rich Harris 6 months ago
commit df3b6c0ca6

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: export `parseCss` from `svelte/compiler`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: trigger `selectedcontent` reactivity

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

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

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

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

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

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

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

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

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

@ -1,6 +0,0 @@
export function busy() {
let a = 0;
for (let i = 0; i < 1_00; i++) {
a++;
}
}

@ -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,177 @@ 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<number>} 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<Source<number>>) => void} fn
* @param {number} count
* @param {number} num_sources
*/
function create_computation_2(s1, s2) {
$.derived(() => $.get(s1) + $.get(s2));
}
function create_sbench_test(label, count, num_sources, fn) {
return {
label,
fn: async () => {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
fn(count, create_sources(num_sources, []));
}
function create_computation_1000(ss, offset) {
$.derived(() => {
let sum = 0;
for (let i = 0; i < 1000; i++) {
sum += $.get(ss[offset + i]);
return await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
fn(count, create_sources(num_sources, []));
}
});
destroy();
});
}
return sum;
});
};
}
/**
* @param {number} n
*/
function create_computations_0to1(n) {
for (let i = 0; i < n; i++) {
create_computation_0(i);
}
}
export const sbench_create_signals = create_sbench_test(
'sbench_create_signals',
COUNT,
COUNT,
create_sources
);
/**
* @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);
}
}
/**
* @param {number} n
* @param {any[]} sources
*/
function create_computations_2to1(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_2(sources[i * 2], sources[i * 2 + 1]);
$.derived(() => i);
}
}
function create_computation_4(s1, s2, s3, s4) {
$.derived(() => $.get(s1) + $.get(s2) + $.get(s3) + $.get(s4));
}
});
function create_computations_1000to1(n, sources) {
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);
}
}
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)
};
}
);

@ -0,0 +1,35 @@
import assert from 'node:assert';
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.equal($.get(computed5), 6);
for (let i = 0; i < 1000; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(computed5), 6);
}
}
};
};

@ -0,0 +1,41 @@
import assert from 'node:assert';
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.equal($.get(last), i + 50);
}
assert.equal(counter, 50 * 50);
}
};
};

@ -0,0 +1,41 @@
import assert from 'node:assert';
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.equal($.get(current), len + i);
}
assert.equal(counter, iter);
}
};
};

@ -0,0 +1,45 @@
import assert from 'node:assert';
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.equal($.get(sum), 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(sum), (i + 1) * width);
}
assert.equal(counter, 500);
}
};
};

@ -0,0 +1,38 @@
import assert from 'node:assert';
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.equal($.get(splited[i]), i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush(() => {
$.set(heads[i], i * 2);
});
assert.equal($.get(splited[i]), i * 2 + 1);
}
}
};
};

@ -0,0 +1,42 @@
import assert from 'node:assert';
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.equal($.get(current), size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(current), i * size);
}
assert.equal(counter, 100);
}
};
};

@ -0,0 +1,55 @@
import assert from 'node:assert';
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.equal($.get(sum), constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(sum), constant - width + i * width);
}
assert.equal(counter, 100);
}
};
};

@ -0,0 +1,41 @@
import assert from 'node:assert';
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.equal($.get(current), 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
}
assert.equal(counter, 100);
}
};
};

@ -1,4 +1,4 @@
import { assert, fastest_test } from '../../utils.js';
import assert from 'node:assert';
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);
@ -59,63 +59,10 @@ function setup() {
$.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);
}
};
}
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)
};
}
};

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

@ -0,0 +1,71 @@
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: {
label: `${label}_unowned`,
fn: 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 result = 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;
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 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",

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

@ -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": {

@ -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<AST.CSS.StyleSheet, 'attributes' | 'content'>}
*/
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

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

@ -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, '</style');
const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length);
const content_end = parser.index;
parser.read(/^<\/style\s*>/);
parser.eat('</style', true);
parser.read(/^\s*>/);
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<AST.CSS.Rule | AST.CSS.Atrule>}
*/
function read_body(parser, close) {
function read_body(parser, finished) {
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
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 `<style>`).
* @param {Parser} parser
* @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
*/
export function parse_stylesheet(parser) {
return read_body(parser, (p) => p.index >= p.template.length);
}

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

@ -0,0 +1,138 @@
import { assert, describe, it } from 'vitest';
import { parseCss } from 'svelte/compiler';
describe('parseCss', () => {
it('parses a simple rule', () => {
const ast = parseCss('div { color: red; }');
assert.equal(ast.type, 'StyleSheet');
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Rule');
});
it('parses at-rules', () => {
const ast = parseCss('@media (min-width: 800px) { div { color: red; } }');
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Atrule');
if (ast.children[0].type === 'Atrule') {
assert.equal(ast.children[0].name, 'media');
}
});
it('parses @import', () => {
const ast = parseCss("@import 'foo.css';");
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Atrule');
if (ast.children[0].type === 'Atrule') {
assert.equal(ast.children[0].name, 'import');
assert.equal(ast.children[0].block, null);
}
});
it('parses multiple rules', () => {
const ast = parseCss('div { color: red; } span { color: blue; }');
assert.equal(ast.children.length, 2);
});
it('has correct start/end positions', () => {
const ast = parseCss('div { color: red; }');
assert.equal(ast.start, 0);
assert.equal(ast.end, 19);
});
it('strips BOM', () => {
const ast = parseCss('\uFEFFdiv { color: red; }');
assert.equal(ast.start, 0);
assert.equal(ast.end, 19);
});
it('parses nested rules', () => {
const ast = parseCss('div { color: red; span { color: blue; } }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
assert.equal(rule.type, 'Rule');
if (rule.type === 'Rule') {
assert.equal(rule.block.children.length, 2); // declaration + nested rule
}
});
it('parses empty stylesheet', () => {
const ast = parseCss('');
assert.equal(ast.type, 'StyleSheet');
assert.equal(ast.children.length, 0);
assert.equal(ast.start, 0);
assert.equal(ast.end, 0);
});
it('parses whitespace-only stylesheet', () => {
const ast = parseCss(' \n\t ');
assert.equal(ast.children.length, 0);
});
it('parses comments', () => {
const ast = parseCss('/* comment */ div { color: red; }');
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Rule');
});
it('parses complex selectors', () => {
const ast = parseCss('div > span + p ~ a { color: red; }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
if (rule.type === 'Rule') {
assert.equal(rule.prelude.type, 'SelectorList');
assert.equal(rule.prelude.children.length, 1);
// div > span + p ~ a has 4 relative selectors
assert.equal(rule.prelude.children[0].children.length, 4);
}
});
it('parses pseudo-classes and pseudo-elements', () => {
const ast = parseCss('div:hover::before { color: red; }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
if (rule.type === 'Rule') {
const selectors = rule.prelude.children[0].children[0].selectors;
assert.equal(selectors.length, 3); // div, :hover, ::before
assert.equal(selectors[0].type, 'TypeSelector');
assert.equal(selectors[1].type, 'PseudoClassSelector');
assert.equal(selectors[2].type, 'PseudoElementSelector');
}
});
it('parses @keyframes', () => {
const ast = parseCss('@keyframes fade { from { opacity: 0; } to { opacity: 1; } }');
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Atrule');
if (ast.children[0].type === 'Atrule') {
assert.equal(ast.children[0].name, 'keyframes');
assert.notEqual(ast.children[0].block, null);
}
});
it('parses class and id selectors', () => {
const ast = parseCss('.foo#bar { color: red; }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
if (rule.type === 'Rule') {
const selectors = rule.prelude.children[0].children[0].selectors;
assert.equal(selectors.length, 2);
assert.equal(selectors[0].type, 'ClassSelector');
assert.equal(selectors[1].type, 'IdSelector');
}
});
it('parses attribute selectors', () => {
const ast = parseCss('[data-foo="bar"] { color: red; }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
if (rule.type === 'Rule') {
const selectors = rule.prelude.children[0].children[0].selectors;
assert.equal(selectors.length, 1);
assert.equal(selectors[0].type, 'AttributeSelector');
if (selectors[0].type === 'AttributeSelector') {
assert.equal(selectors[0].name, 'data-foo');
assert.equal(selectors[0].value, 'bar');
}
}
});
});

@ -884,6 +884,12 @@ declare module 'svelte/compiler' {
modern?: false;
loose?: boolean;
} | undefined): Record<string, any>;
/**
* The parseCss function parses a CSS stylesheet, returning its abstract syntax tree.
*
* @param source The CSS source code
* */
export function parseCss(source: string): Omit<AST.CSS.StyleSheet, "attributes" | "content">;
/**
* @deprecated Replace this with `import { walk } from 'estree-walker'`
* */

Loading…
Cancel
Save