perf: avoid O(n²) name scanning in scope generate/unique (#17844)

## Summary

`Scope.generate()` and `ScopeRoot.unique()` search for available names
by iterating from suffix `_1` upward. When the same preferred name is
generated many times (e.g. `text` is generated 482 times in a large
component), the Nth call re-scans all N-1 already-taken names — O(n²)
total work.

This adds a `#name_counters` Map to `ScopeRoot` that tracks the next
suffix to try per name, so each call resumes from where the last one
left off. Generated names are identical to before.

## Benchmark (interleaved, best-of-3 rounds)

| Component | Min | Median |
|---|---|---|
| Realistic (~80 lines) | ~1% | ~7% |
| Medium (316 lines) | ~1% | ~5% |
| Large (642 lines) | ~7% | ~2% |
| XLarge (1302 lines) | **~11%** | **~10%** |

## Test plan

- [x] All snapshot tests pass (name generation unchanged)
- [x] All validator, compiler-error, runtime-runes, runtime-legacy tests
pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
pull/17842/head
Mathias Picker 5 months ago committed by GitHub
parent 2f12b60701
commit 32111f9e84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: avoid O(n²) name scanning in scope `generate` and `unique`

@ -713,8 +713,18 @@ export class Scope {
}
preferred_name = preferred_name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_');
let name = preferred_name;
let n = 1;
// Use cached counter to skip names already known to be taken (avoids O(n²) scanning)
let n = this.root.next_counter(preferred_name);
let name;
if (n === 0) {
name = preferred_name;
n = 1;
} else {
name = `${preferred_name}_${n}`;
n++;
}
while (
this.references.has(name) ||
@ -725,6 +735,7 @@ export class Scope {
name = `${preferred_name}_${n++}`;
}
this.root.set_counter(preferred_name, n);
this.references.set(name, []);
this.root.conflicts.add(name);
return name;
@ -852,18 +863,49 @@ export class ScopeRoot {
/** @type {Set<string>} */
conflicts = new Set();
/**
* Tracks the next suffix counter per name to avoid O(n) rescanning in generate/unique.
* @type {Map<string, number>}
*/
#name_counters = new Map();
/**
* @param {string} name
* @returns {number}
*/
next_counter(name) {
return this.#name_counters.get(name) ?? 0;
}
/**
* @param {string} name
* @param {number} value
*/
set_counter(name, value) {
this.#name_counters.set(name, value);
}
/**
* @param {string} preferred_name
*/
unique(preferred_name) {
preferred_name = preferred_name.replace(/[^a-zA-Z0-9_$]/g, '_');
let final_name = preferred_name;
let n = 1;
let n = this.#name_counters.get(preferred_name) ?? 0;
let final_name;
if (n === 0) {
final_name = preferred_name;
n = 1;
} else {
final_name = `${preferred_name}_${n}`;
n++;
}
while (this.conflicts.has(final_name)) {
final_name = `${preferred_name}_${n++}`;
}
this.#name_counters.set(preferred_name, n);
this.conflicts.add(final_name);
const id = b.id(final_name);
return id;

Loading…
Cancel
Save