mirror of https://github.com/sveltejs/svelte
commit
35effcb119
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
name: performance-investigation
|
||||||
|
description: Investigate performance regressions and find opportunities for optimization
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
1. Start from a branch you want to measure (for example `foo`).
|
||||||
|
2. Run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm bench:compare main foo
|
||||||
|
```
|
||||||
|
|
||||||
|
If you pass one branch, `bench:compare` automatically compares it to `main`.
|
||||||
|
|
||||||
|
## Where outputs go
|
||||||
|
|
||||||
|
- Summary report: `benchmarking/compare/.results/report.txt`
|
||||||
|
- Raw benchmark numbers:
|
||||||
|
- `benchmarking/compare/.results/main.json`
|
||||||
|
- `benchmarking/compare/.results/<your-branch>.json`
|
||||||
|
- CPU profiles (per benchmark, per branch):
|
||||||
|
- `benchmarking/compare/.profiles/main/*.cpuprofile`
|
||||||
|
- `benchmarking/compare/.profiles/main/*.md`
|
||||||
|
- `benchmarking/compare/.profiles/<your-branch>/*.cpuprofile`
|
||||||
|
- `benchmarking/compare/.profiles/<your-branch>/*.md`
|
||||||
|
|
||||||
|
The `.md` files are generated summaries of the CPU profile and are usually the fastest way to inspect hotspots.
|
||||||
|
|
||||||
|
## Suggested investigation flow
|
||||||
|
|
||||||
|
1. Open `benchmarking/compare/.results/report.txt` and identify largest regressions first.
|
||||||
|
2. For each high-delta benchmark, compare:
|
||||||
|
- `benchmarking/compare/.profiles/main/<benchmark>.md`
|
||||||
|
- `benchmarking/compare/.profiles/<branch>/<benchmark>.md`
|
||||||
|
3. Look for changes in self/inclusive hotspot share in runtime internals (`runtime.js`, `reactivity/batch.js`, `reactivity/deriveds.js`, `reactivity/sources.js`).
|
||||||
|
4. Make one optimization change at a time, then re-run targeted benches before re-running full compare.
|
||||||
|
|
||||||
|
## Fast benchmark loops
|
||||||
|
|
||||||
|
Run only selected reactivity benchmarks by substring:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm bench kairo_mux kairo_deep kairo_broad kairo_triangle
|
||||||
|
pnpm bench repeated_deps sbench_create_signals mol_owned
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests to run after perf changes
|
||||||
|
|
||||||
|
Runtime reactivity regressions are most likely in runes runtime tests:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm test runtime-runes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helpful script
|
||||||
|
|
||||||
|
For quick cpuprofile hotspot deltas between two branches:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node benchmarking/compare/profile-diff.mjs kairo_mux_owned main foo
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints top function sample-share deltas for the selected benchmark.
|
||||||
|
|
||||||
|
## Practical gotchas
|
||||||
|
|
||||||
|
- `bench:compare` checks out branches while running. Avoid uncommitted changes (or stash them) so branch switching is safe.
|
||||||
|
- Each `bench:compare` run rewrites `benchmarking/compare/.results` and `benchmarking/compare/.profiles`.
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: invalidate `@const` tags based on visible references in legacy mode
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: disallow `--` in `idPrefix`
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: correct types for `ontoggle` on `<details>` elements
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: don't override `$destroy/set/on` instance methods in dev mode
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: unskip branches of earlier batches after commit
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: never set derived.v inside fork
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: skip rebase logic in non-async mode
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: don't reset status of uninitialized deriveds
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
# Svelte Coding Agent Guide
|
||||||
|
|
||||||
|
This guide is for AI coding agents working in the Svelte monorepo.
|
||||||
|
|
||||||
|
**Important:** Read and follow [`CONTRIBUTING.md`](./CONTRIBUTING.md) as well - it contains essential information about testing, code structure, and contribution guidelines that applies here.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
If asked to do a performance investigation, use the `performance-investigation` skill.
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import assert from 'node:assert';
|
||||||
|
import * as $ from 'svelte/internal/client';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const a = $.state(1);
|
||||||
|
const b = $.state(2);
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
const destroy = $.effect_root(() => {
|
||||||
|
for (let i = 0; i < 1000; i += 1) {
|
||||||
|
$.render_effect(() => {
|
||||||
|
total += $.get(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.render_effect(() => {
|
||||||
|
total += $.get(b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy,
|
||||||
|
run() {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
total = 0;
|
||||||
|
$.flush(() => $.set(b, i));
|
||||||
|
assert.equal(total, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
|
export function generate_report(outdir) {
|
||||||
|
const result_files = fs
|
||||||
|
.readdirSync(outdir)
|
||||||
|
.filter((file) => file.endsWith('.json'))
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
const branches = result_files.map((file) => file.slice(0, -5));
|
||||||
|
const results = result_files.map((file) =>
|
||||||
|
JSON.parse(fs.readFileSync(`${outdir}/${file}`, 'utf-8'))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
console.error(`No result files found in ${outdir}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const report_file = path.join(outdir, 'report.txt');
|
||||||
|
|
||||||
|
fs.writeFileSync(report_file, '');
|
||||||
|
|
||||||
|
const write = (str) => {
|
||||||
|
fs.appendFileSync(report_file, str + '\n');
|
||||||
|
console.log(str);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < branches.length; i += 1) {
|
||||||
|
write(`${char(i)}: ${branches[i]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
write('');
|
||||||
|
|
||||||
|
for (let i = 0; i < results[0].length; i += 1) {
|
||||||
|
write(`${results[0][i].benchmark}`);
|
||||||
|
|
||||||
|
for (const metric of ['time', 'gc_time']) {
|
||||||
|
const times = results.map((result) => +result[i][metric]);
|
||||||
|
let min = Infinity;
|
||||||
|
let max = -Infinity;
|
||||||
|
let min_index = -1;
|
||||||
|
|
||||||
|
for (let b = 0; b < times.length; b += 1) {
|
||||||
|
const time = times[b];
|
||||||
|
|
||||||
|
if (time < min) {
|
||||||
|
min = time;
|
||||||
|
min_index = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time > max) {
|
||||||
|
max = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min !== 0) {
|
||||||
|
write(` ${metric}: fastest is ${char(min_index)} (${branches[min_index]})`);
|
||||||
|
times.forEach((time, b) => {
|
||||||
|
const SIZE = 20;
|
||||||
|
const n = Math.round(SIZE * (time / max));
|
||||||
|
|
||||||
|
write(` ${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function char(i) {
|
||||||
|
return String.fromCharCode(97 + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||||
|
const outdir = path.resolve(process.argv[1], '../.results');
|
||||||
|
|
||||||
|
generate_report(outdir);
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const [benchmark, baseBranch = 'main', candidateBranch] = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (!benchmark || !candidateBranch) {
|
||||||
|
console.error(
|
||||||
|
'Usage: node benchmarking/compare/profile-diff.mjs <benchmark> <base-branch> <candidate-branch>'
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = path.resolve('benchmarking/compare/.profiles');
|
||||||
|
|
||||||
|
function safe(name) {
|
||||||
|
return name.replace(/[^a-z0-9._-]+/gi, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
function read_profile(branch, bench) {
|
||||||
|
const file = path.join(root, safe(branch), `${bench}.cpuprofile`);
|
||||||
|
const profile = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
|
const nodes = Array.isArray(profile.nodes) ? profile.nodes : [];
|
||||||
|
const samples = Array.isArray(profile.samples) ? profile.samples : [];
|
||||||
|
|
||||||
|
const id_to_node = new Map(nodes.map((node) => [node.id, node]));
|
||||||
|
const self_counts = new Map();
|
||||||
|
|
||||||
|
for (const sample of samples) {
|
||||||
|
if (typeof sample !== 'number') continue;
|
||||||
|
self_counts.set(sample, (self_counts.get(sample) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = samples.length || 1;
|
||||||
|
const by_fn = new Map();
|
||||||
|
|
||||||
|
for (const [id, count] of self_counts) {
|
||||||
|
const node = id_to_node.get(id);
|
||||||
|
if (!node || typeof node !== 'object') continue;
|
||||||
|
|
||||||
|
const frame = node.callFrame ?? {};
|
||||||
|
const function_name = frame.functionName || '(anonymous)';
|
||||||
|
const url = frame.url || '';
|
||||||
|
const line = typeof frame.lineNumber === 'number' ? frame.lineNumber + 1 : 0;
|
||||||
|
|
||||||
|
const label = url
|
||||||
|
? `${function_name} @ ${url.replace(/^.*packages\//, 'packages/')}:${line}`
|
||||||
|
: function_name;
|
||||||
|
|
||||||
|
by_fn.set(label, (by_fn.get(label) ?? 0) + count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { by_fn, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = read_profile(baseBranch, benchmark);
|
||||||
|
const candidate = read_profile(candidateBranch, benchmark);
|
||||||
|
|
||||||
|
const keys = new Set([...base.by_fn.keys(), ...candidate.by_fn.keys()]);
|
||||||
|
const rows = [...keys]
|
||||||
|
.map((key) => {
|
||||||
|
const base_pct = ((base.by_fn.get(key) ?? 0) * 100) / base.total;
|
||||||
|
const candidate_pct = ((candidate.by_fn.get(key) ?? 0) * 100) / candidate.total;
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
delta: candidate_pct - base_pct,
|
||||||
|
base_pct,
|
||||||
|
candidate_pct
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta))
|
||||||
|
.slice(0, 20);
|
||||||
|
|
||||||
|
console.log(`Benchmark: ${benchmark}`);
|
||||||
|
console.log(`Base: ${baseBranch}`);
|
||||||
|
console.log(`Candidate: ${candidateBranch}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const sign = row.delta >= 0 ? '+' : '';
|
||||||
|
console.log(
|
||||||
|
`${sign}${row.delta.toFixed(2).padStart(6)}pp candidate ${row.candidate_pct.toFixed(2).padStart(6)}% base ${row.base_pct.toFixed(2).padStart(6)}% ${row.key}`
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<h1>Hello</h1>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
compileOptions: {
|
||||||
|
hmr: true
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import Component from "./Component.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Component --color="red" />
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: `
|
||||||
|
<input>
|
||||||
|
<p>hello</p>
|
||||||
|
<p>hello</p>
|
||||||
|
`,
|
||||||
|
|
||||||
|
ssrHtml: `
|
||||||
|
<input value="hello">
|
||||||
|
<p>hello</p>
|
||||||
|
<p>hello</p>
|
||||||
|
`,
|
||||||
|
|
||||||
|
async test({ assert, target }) {
|
||||||
|
const [input] = target.querySelectorAll('input');
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
input.value = 'goodbye';
|
||||||
|
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<input>
|
||||||
|
<p>goodbye</p>
|
||||||
|
<p>goodbye</p>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<svelte:options runes={false} />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let message = 'hello';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input bind:value={message} />
|
||||||
|
|
||||||
|
{#if true}
|
||||||
|
{@const m1 = message}
|
||||||
|
{@const m2 = (() => m1)()}
|
||||||
|
|
||||||
|
<p>{m1}</p>
|
||||||
|
<p>{m2}</p>
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<svelte:options customElement={{
|
||||||
|
tag: "my-inner",
|
||||||
|
props: { value: { reflect: true }}
|
||||||
|
}} />
|
||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
</script>
|
||||||
|
{value}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await tick();
|
||||||
|
const [btn] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
btn.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<button>inc</button>
|
||||||
|
<my-inner value="2"></my-inner>
|
||||||
|
2
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
import "./Inner.svelte"
|
||||||
|
let count = 1;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={() => count++}>inc</button>
|
||||||
|
<!-- updating value prop will cause a flushSync -->
|
||||||
|
<my-inner value={count}></my-inner>
|
||||||
|
<!-- updating count will cause an internal_set -->
|
||||||
|
{#each [count] as row}
|
||||||
|
{row}
|
||||||
|
{/each}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await tick();
|
||||||
|
const [a, b, resolve] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
a.click();
|
||||||
|
await tick();
|
||||||
|
b.click();
|
||||||
|
await tick();
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<button>a</button>
|
||||||
|
<button>b</button>
|
||||||
|
<button>resolve</button>
|
||||||
|
hi
|
||||||
|
1
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
let a = $state(0);
|
||||||
|
let b = $state(0);
|
||||||
|
let a_b = $derived(a * b);
|
||||||
|
|
||||||
|
const queued = [];
|
||||||
|
|
||||||
|
function push(value) {
|
||||||
|
if (!value) return value;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
queued.push(() => resolve(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => (a++)}>a</button>
|
||||||
|
<button onclick={() => (b++)}>b</button>
|
||||||
|
<button onclick={() => (queued.shift()?.())}>resolve</button>
|
||||||
|
<!-- a_b called in a block effect before being called in an async effect -->
|
||||||
|
{#if a_b}hi{/if}
|
||||||
|
{await push(a_b)}
|
||||||
@ -1,102 +0,0 @@
|
|||||||
import { tick } from 'svelte';
|
|
||||||
import { test } from '../../test';
|
|
||||||
|
|
||||||
export default test({
|
|
||||||
skip: true,
|
|
||||||
async test({ assert, target }) {
|
|
||||||
const [add, shift] = target.querySelectorAll('button');
|
|
||||||
|
|
||||||
add.click();
|
|
||||||
await tick();
|
|
||||||
add.click();
|
|
||||||
await tick();
|
|
||||||
add.click();
|
|
||||||
await tick();
|
|
||||||
|
|
||||||
// TODO pending count / number of pushes is off
|
|
||||||
|
|
||||||
assert.htmlEqual(
|
|
||||||
target.innerHTML,
|
|
||||||
`
|
|
||||||
<button>add</button>
|
|
||||||
<button>shift</button>
|
|
||||||
<p>pending=6 values.length=1 values=[1]</p>
|
|
||||||
<div>not keyed:
|
|
||||||
<div>1</div>
|
|
||||||
</div>
|
|
||||||
<div>keyed:
|
|
||||||
<div>1</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
shift.click();
|
|
||||||
await tick();
|
|
||||||
shift.click();
|
|
||||||
await tick();
|
|
||||||
assert.htmlEqual(
|
|
||||||
target.innerHTML,
|
|
||||||
`
|
|
||||||
<button>add</button>
|
|
||||||
<button>shift</button>
|
|
||||||
<p>pending=4 values.length=2 values=[1,2]</p>
|
|
||||||
<div>not keyed:
|
|
||||||
<div>1</div>
|
|
||||||
<div>2</div>
|
|
||||||
</div>
|
|
||||||
<div>keyed:
|
|
||||||
<div>1</div>
|
|
||||||
<div>2</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
shift.click();
|
|
||||||
await tick();
|
|
||||||
shift.click();
|
|
||||||
await tick();
|
|
||||||
assert.htmlEqual(
|
|
||||||
target.innerHTML,
|
|
||||||
`
|
|
||||||
<button>add</button>
|
|
||||||
<button>shift</button>
|
|
||||||
<p>pending=2 values.length=3 values=[1,2,3]</p>
|
|
||||||
<div>not keyed:
|
|
||||||
<div>1</div>
|
|
||||||
<div>2</div>
|
|
||||||
<div>3</div>
|
|
||||||
</div>
|
|
||||||
<div>keyed:
|
|
||||||
<div>1</div>
|
|
||||||
<div>2</div>
|
|
||||||
<div>3</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
shift.click();
|
|
||||||
await tick();
|
|
||||||
shift.click();
|
|
||||||
await tick();
|
|
||||||
assert.htmlEqual(
|
|
||||||
target.innerHTML,
|
|
||||||
`
|
|
||||||
<button>add</button>
|
|
||||||
<button>shift</button>
|
|
||||||
<p>pending=0 values.length=4 values=[1,2,3,4]</p>
|
|
||||||
<div>not keyed:
|
|
||||||
<div>1</div>
|
|
||||||
<div>2</div>
|
|
||||||
<div>3</div>
|
|
||||||
<div>4</div>
|
|
||||||
</div>
|
|
||||||
<div>keyed:
|
|
||||||
<div>1</div>
|
|
||||||
<div>2</div>
|
|
||||||
<div>3</div>
|
|
||||||
<div>4</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
const [, toggle] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
toggle?.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>0</button> <button>toggle</button> 0`);
|
||||||
|
|
||||||
|
toggle?.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>0</button> <button>toggle</button>`);
|
||||||
|
|
||||||
|
toggle?.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>0</button> <button>toggle</button> 0`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
import { fork } from 'svelte';
|
||||||
|
let show = $state(false);
|
||||||
|
let count = $state(0);
|
||||||
|
let d_count = $derived(count);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => count += 1}>{count}</button> <!-- just here so count is compiled as a source -->
|
||||||
|
<button onclick={() => fork(() => show = !show).commit()}>toggle</button>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
{d_count}
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await tick();
|
||||||
|
const [x, y, resolve, commit] = target.querySelectorAll('button');
|
||||||
|
const [p] = target.querySelectorAll('p');
|
||||||
|
|
||||||
|
y.click();
|
||||||
|
await tick();
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
x.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(p.innerHTML, '1 0');
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
commit.click();
|
||||||
|
assert.htmlEqual(p.innerHTML, '1 1');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import { fork } from 'svelte';
|
||||||
|
|
||||||
|
let x = $state(0);
|
||||||
|
let y = $state(0);
|
||||||
|
let f;
|
||||||
|
|
||||||
|
const deferred = [];
|
||||||
|
|
||||||
|
function delay(value) {
|
||||||
|
if (!value) return value;
|
||||||
|
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>{x} {await delay(y)}</p>
|
||||||
|
|
||||||
|
<button onclick={() => x += 1}>x</button>
|
||||||
|
<button onclick={() => f = fork(() => y += 1)}>y (fork)</button>
|
||||||
|
<button onclick={() => deferred.shift()?.()}>resolve</button>
|
||||||
|
<button onclick={() => f.commit()}>commit</button>
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
mode: ['hydrate'],
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `<div><div><p>first test</p></div> other test</div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
function firstTest() {
|
||||||
|
return Promise.resolve('<p>first test</p>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function otherTest() {
|
||||||
|
return Promise.resolve('other test');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>{@html await firstTest()}</div>
|
||||||
|
{await otherTest()}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
let data = $derived(await Promise.resolve('test'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div data-resolved={data ? 'true' : 'false'}>
|
||||||
|
{data}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import Bound from './Bound.svelte';
|
||||||
|
|
||||||
|
let open;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Bound bind:open />
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
// Tests that renderer.subsume (which is used when bindings are present) works correctly
|
||||||
|
export default test({
|
||||||
|
mode: ['hydrate'],
|
||||||
|
html: '<div data-resolved="true">test</div>',
|
||||||
|
async test({ assert, warnings }) {
|
||||||
|
assert.deepEqual(warnings, []);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Async from './Async.svelte';
|
||||||
|
import Binding from './Binding.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Async />
|
||||||
|
<Binding />
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await tick();
|
||||||
|
const [load, resolve] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
load.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<button>load</button>
|
||||||
|
<button>resolve</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
search search search search
|
||||||
|
<button>load</button>
|
||||||
|
<button>resolve</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
<script>
|
||||||
|
let query = $state('');
|
||||||
|
// changing the query results in a new promise with loading initialized to true
|
||||||
|
const promise = $derived(push(query));
|
||||||
|
|
||||||
|
const resolvers = [];
|
||||||
|
function push(value) {
|
||||||
|
if (!value) return Promise.resolve(value);
|
||||||
|
|
||||||
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
|
|
||||||
|
resolvers.push(() => {
|
||||||
|
// before resolving, set loading to false - this makes it run in a different batch
|
||||||
|
loading = false;
|
||||||
|
resolve(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
let loading = $state(true);
|
||||||
|
Object.defineProperty(promise, 'loading', {
|
||||||
|
get() {
|
||||||
|
return loading;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{query} {await promise}
|
||||||
|
|
||||||
|
{#if !promise.loading}
|
||||||
|
{query}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !promise.loading}
|
||||||
|
{await query}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button onclick={() => query = 'search'}>load</button>
|
||||||
|
<button onclick={() => resolvers.shift()?.()}>resolve</button>
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
const [a, b] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>a 0</button><button>b 0</button><p>hello</p>`);
|
||||||
|
|
||||||
|
a.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>a 0</button><button>b 0</button><p>hello</p>`);
|
||||||
|
|
||||||
|
a.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>a 2</button><button>b 0</button><p>hello</p>`);
|
||||||
|
|
||||||
|
a.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>a 2</button><button>b 0</button><p>hello</p>`);
|
||||||
|
|
||||||
|
// if we don't skip over the never-resolving promise in the `else` block, we will never update
|
||||||
|
b.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>a 3</button><button>b 1</button><p>hello</p>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
let a = $state(0);
|
||||||
|
let b = $state(0);
|
||||||
|
let show = $state(true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => (a++, show = !show)}>a {a}</button>
|
||||||
|
<button onclick={() => (b++, show = !show)}>b {b}</button>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<p>hello</p>
|
||||||
|
{:else}
|
||||||
|
{await new Promise(() => {})}
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await tick();
|
||||||
|
const [a_b, b, resolve] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
a_b.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
|
||||||
|
);
|
||||||
|
|
||||||
|
b.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
|
||||||
|
);
|
||||||
|
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
'<button>a_b 1_2</button> <button>b 2</button> <button>resolve</button> 1'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
let a = $state(0);
|
||||||
|
let b = $state(0);
|
||||||
|
let deferreds = [];
|
||||||
|
|
||||||
|
function push(value) {
|
||||||
|
if (!value) return value;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
deferreds.push(() => resolve(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => {a++;b++}}>a_b {a}_{b}</button>
|
||||||
|
<button onclick={() => (b++)}>b {b}</button>
|
||||||
|
<button onclick={() => (deferreds.shift()?.())}>resolve</button>
|
||||||
|
{await push(a)}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await tick();
|
||||||
|
const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
a_b.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
a 0 | b 0 | c 0 | d 0
|
||||||
|
<button>a and b</button>
|
||||||
|
<button>a and c</button>
|
||||||
|
<button>b and d</button>
|
||||||
|
<button>shift</button>
|
||||||
|
<button>pop</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
a_c.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
a 0 | b 0 | c 0 | d 0
|
||||||
|
<button>a and b</button>
|
||||||
|
<button>a and c</button>
|
||||||
|
<button>b and d</button>
|
||||||
|
<button>shift</button>
|
||||||
|
<button>pop</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
b_d.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
a 0 | b 0 | c 0 | d 0
|
||||||
|
<button>a and b</button>
|
||||||
|
<button>a and c</button>
|
||||||
|
<button>b and d</button>
|
||||||
|
<button>shift</button>
|
||||||
|
<button>pop</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
a 0 | b 0 | c 0 | d 0
|
||||||
|
<button>a and b</button>
|
||||||
|
<button>a and c</button>
|
||||||
|
<button>b and d</button>
|
||||||
|
<button>shift</button>
|
||||||
|
<button>pop</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
a 1 | b 1 | c 0 | d 0
|
||||||
|
<button>a and b</button>
|
||||||
|
<button>a and c</button>
|
||||||
|
<button>b and d</button>
|
||||||
|
<button>shift</button>
|
||||||
|
<button>pop</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
a 2 | b 1 | c 1 | d 0
|
||||||
|
<button>a and b</button>
|
||||||
|
<button>a and c</button>
|
||||||
|
<button>b and d</button>
|
||||||
|
<button>shift</button>
|
||||||
|
<button>pop</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
a 2 | b 2 | c 1 | d 1
|
||||||
|
<button>a and b</button>
|
||||||
|
<button>a and c</button>
|
||||||
|
<button>b and d</button>
|
||||||
|
<button>shift</button>
|
||||||
|
<button>pop</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<script>
|
||||||
|
let a = $state(0);
|
||||||
|
let b = $state(0);
|
||||||
|
let c = $state(0);
|
||||||
|
let d = $state(0);
|
||||||
|
|
||||||
|
const deferred = [];
|
||||||
|
|
||||||
|
function delay(value) {
|
||||||
|
if (!value) return value;
|
||||||
|
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
a {await delay(a)} | b {await delay(b)} | c {c} | d {d}
|
||||||
|
<button onclick={() => {a++;b++;}}>
|
||||||
|
a and b
|
||||||
|
</button>
|
||||||
|
<button onclick={() => {a++;c++;}}>
|
||||||
|
a and c
|
||||||
|
</button>
|
||||||
|
<button onclick={() => {b++;d++;}}>
|
||||||
|
b and d
|
||||||
|
</button>
|
||||||
|
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||||
|
<button onclick={() => deferred.pop()?.()}>pop</button>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue