perf: make async blocker analysis linear (#18549)

`trace_references` recreated a fresh seen set for every CallExpression,
so `touch` re-walked the same transitive assignment graph once per call.
For N calls reaching an N-deep binding chain, this was $O(N^2)$.

Share one seen set per trace_references invocation. Use separate sets
for the write-directed CallExpression touches and the read-directed
ReturnStatement touches. The shared set ensures each assignment-value
expression is walked at most once, making the traversal linear. `touch`
only ever adds to a fixed target set, so skipping an already-seen
expression never drops a binding: compiler output is byte-identical.

Complements #18548.

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/18537/merge
Joe Schafer 3 weeks ago committed by GitHub
parent 950e2a837c
commit 545205b420
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: make async blocker analysis scale linearly with the number of top-level references

@ -958,7 +958,7 @@ function calculate_blockers(instance, analysis) {
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
const touch = (expression, scope, touched, seen) => {
if (seen.has(expression)) return;
seen.add(expression);
@ -1015,6 +1015,13 @@ function calculate_blockers(instance, analysis) {
}
}
// Share seen nodes across calls so transitive assignments are only visited once.
// Keep separate read/write state because the target sets can differ.
/** @type {Set<ESTree.Node>} */
const writes_seen = new Set();
/** @type {Set<ESTree.Node>} */
const reads_seen = new Set();
walk(
node,
{ scope },
@ -1044,13 +1051,7 @@ function calculate_blockers(instance, analysis) {
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
touch(node, context.state.scope, writes, writes_seen);
},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
@ -1066,7 +1067,7 @@ function calculate_blockers(instance, analysis) {
// might be called immediately, so we have to touch all references within it. Example:
// function foo() { return () => blocker; } foo(); // blocker is touched
if (node.argument) {
touch(node.argument, context.state.scope, reads);
touch(node.argument, context.state.scope, reads, reads_seen);
}
},
// don't look inside functions until they are called
@ -1242,7 +1243,7 @@ function calculate_blockers(instance, analysis) {
} else {
// A concise arrow body is an implicit return, so treat it like the
// `ReturnStatement` visitor in `trace_references` would.
touch(init.body, fn_scope, reads_writes);
touch(init.body, fn_scope, reads_writes, new Set());
}
const max = [...reads_writes].reduce((max, binding) => {

Loading…
Cancel
Save