fix(resolution): gate closure-collection synthesis to Swift/Kotlin and de-quadratic its line accounting (#1235) (#1237)
closureCollectionEdges scanned every method/function node in every
language, but its dispatcher patterns ({ $0( / { it( ) are Swift/Kotlin
trailing-closure syntax — on PHP/JS repos the pass can never emit an
edge, yet .push(/.add( fired its append gate on nearly every function.
Per match it computed the line via src.slice(0, idx).split('\n'), which
is O(source) per match and goes quadratic on match-dense generated
functions (two-byte content roughly doubles it). On a 12,860-file
PHP/JS app that was 20+ minutes of the "Resolving refs" tail — frozen
at 97% — and a #850 watchdog kill; profiled on CRMEB it was 127s of a
166s index for zero edges.
- Skip nodes whose language isn't swift/kotlin before any file I/O.
- makeLineAt(): lazy newline index + binary search, shared with the
emitter passes' per-file lineOf.
- Yield every 256 regex matches inside the scan's match loops so a
single pathological function can't starve the watchdog.
CRMEB (ThinkPHP, 2,913 files): 166.8s -> 72.7s, graph byte-identical.
Alamofire: closure-collection edges byte-identical (9 edges, 4 fields).
Drupal core control: graph byte-identical.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e552dc2d2f
commit
e76a355df5
@@ -66,6 +66,15 @@ const CC_DISPATCH_RE = /(\w+)\.forEach\s*\{\s*(?:\$0|it)\s*\(/g;
|
||||
const CC_APPEND_WRITE_RE = /(\w+)\.write\s*\{\s*\$0(?:\.(\w+))?\.(?:append|add|push|insert)\s*\(/g;
|
||||
const CC_APPEND_DIRECT_RE = /(\w+)\.(?:append|add|push|insert)\s*\(/g;
|
||||
const CC_FANOUT_CAP = 8; // skip a field name with more dispatchers/registrars than this (too generic to pair confidently)
|
||||
// The dispatcher gate — `{ $0( ` / `{ it( ` element-invocation — is Swift/Kotlin
|
||||
// trailing-closure syntax, so ONLY those languages can ever contribute a
|
||||
// dispatcher, and a cross-language registrar pairing (a JS `.push(` against a
|
||||
// Swift dispatcher's field name) would be a wrong edge, not a missed one.
|
||||
// Gating both sides here isn't just precision: `.push(`/`.add(` is everywhere
|
||||
// in JS/PHP, so an ungated scan slices + regexes nearly every function on repos
|
||||
// where the pass cannot emit a single edge — on a 12k-file PHP/JS app that was
|
||||
// 20+ minutes of the "Resolving refs" tail and a #850 watchdog kill (#1235).
|
||||
const CC_LANGUAGES = new Set(['swift', 'kotlin']);
|
||||
|
||||
function kebabToPascal(s: string): string {
|
||||
return s.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
||||
@@ -100,6 +109,33 @@ function sliceLines(content: string, startLine?: number, endLine?: number): stri
|
||||
return content.split('\n').slice(startLine - 1, endLine).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-match line resolver over `src`, 1-based at `baseLine`. The inline
|
||||
* `src.slice(0, idx).split('\n').length` idiom is O(source-length) PER MATCH,
|
||||
* which goes quadratic on a match-dense source (a generated function full of
|
||||
* `.push(` calls re-scanned tens of thousands of times was most of the #1235
|
||||
* indexing wedge). Builds the newline index once — lazily, since most sources
|
||||
* never produce a match — then answers each call with a binary search.
|
||||
*/
|
||||
function makeLineAt(src: string, baseLine: number): (idx: number) => number {
|
||||
let nl: number[] | null = null;
|
||||
return (idx: number) => {
|
||||
if (!nl) {
|
||||
nl = [];
|
||||
for (let i = src.indexOf('\n'); i !== -1; i = src.indexOf('\n', i + 1)) nl.push(i);
|
||||
}
|
||||
// Count newlines strictly before idx.
|
||||
let lo = 0;
|
||||
let hi = nl.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (nl[mid]! < idx) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
return baseLine + lo;
|
||||
};
|
||||
}
|
||||
|
||||
function registrarField(src: string): string | null {
|
||||
const m = src.match(/this\.(\w+)\.(?:add|push|set)\(/);
|
||||
return m ? m[1]! : null;
|
||||
@@ -224,24 +260,29 @@ async function closureCollectionEdges(queries: QueryBuilder, ctx: ResolutionCont
|
||||
registrars.set(field, arr);
|
||||
};
|
||||
|
||||
// Slices EVERY method/function's source (no cheap name-gate), so on a repo
|
||||
// with a huge file this is the heaviest synthesis pass — yield mid-scan so it
|
||||
// can't wedge the #850 watchdog on its own (#1091).
|
||||
// Slices EVERY Swift/Kotlin method/function's source (no cheap name-gate), so
|
||||
// on a repo with a huge file this is the heaviest synthesis pass — yield
|
||||
// mid-scan (and mid-match-loop below: a single generated function dense with
|
||||
// matches must not starve the watchdog either) so it can't wedge the #850
|
||||
// watchdog on its own (#1091, #1235).
|
||||
let scanned = 0;
|
||||
let matchTick = 0;
|
||||
for (const m of methodAndFunctionNodes(queries)) {
|
||||
if ((++scanned & 127) === 0) await onYield();
|
||||
if (!CC_LANGUAGES.has(m.language)) continue;
|
||||
const content = ctx.readFile(m.filePath);
|
||||
const src = content && sliceLines(content, m.startLine, m.endLine);
|
||||
if (!src) continue;
|
||||
const hasForEach = src.includes('.forEach');
|
||||
const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert(');
|
||||
if (!hasForEach && !hasAppend) continue;
|
||||
const lineAt = (idx: number) => (m.startLine ?? 1) + src.slice(0, idx).split('\n').length - 1;
|
||||
const lineAt = makeLineAt(src, m.startLine ?? 1);
|
||||
|
||||
if (hasForEach) {
|
||||
CC_DISPATCH_RE.lastIndex = 0;
|
||||
let d: RegExpExecArray | null;
|
||||
while ((d = CC_DISPATCH_RE.exec(src))) {
|
||||
if ((++matchTick & 255) === 0) await onYield();
|
||||
const arr = dispatchers.get(d[1]!) ?? [];
|
||||
if (!arr.some((n) => n.node.id === m.id)) arr.push({ node: m, line: lineAt(d.index) });
|
||||
dispatchers.set(d[1]!, arr);
|
||||
@@ -250,10 +291,16 @@ async function closureCollectionEdges(queries: QueryBuilder, ctx: ResolutionCont
|
||||
if (hasAppend) {
|
||||
CC_APPEND_WRITE_RE.lastIndex = 0;
|
||||
let w: RegExpExecArray | null;
|
||||
while ((w = CC_APPEND_WRITE_RE.exec(src))) addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver
|
||||
while ((w = CC_APPEND_WRITE_RE.exec(src))) {
|
||||
if ((++matchTick & 255) === 0) await onYield();
|
||||
addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver
|
||||
}
|
||||
CC_APPEND_DIRECT_RE.lastIndex = 0;
|
||||
let a: RegExpExecArray | null;
|
||||
while ((a = CC_APPEND_DIRECT_RE.exec(src))) addReg(a[1], m, lineAt(a.index));
|
||||
while ((a = CC_APPEND_DIRECT_RE.exec(src))) {
|
||||
if ((++matchTick & 255) === 0) await onYield();
|
||||
addReg(a[1], m, lineAt(a.index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +341,7 @@ async function eventEmitterEdges(ctx: ResolutionContext, onYield: MaybeYield): P
|
||||
const hasOn = content.includes('.on(') || content.includes('.once(') || content.includes('.addListener(');
|
||||
if (!hasEmit && !hasOn) continue;
|
||||
const nodesInFile = ctx.getNodesInFile(file);
|
||||
const lineOf = (idx: number) => content.slice(0, idx).split('\n').length;
|
||||
const lineOf = makeLineAt(content, 1);
|
||||
|
||||
if (hasEmit) {
|
||||
EMIT_RE.lastIndex = 0;
|
||||
@@ -1368,7 +1415,7 @@ async function rnEventEdges(ctx: ResolutionContext, onYield: MaybeYield): Promis
|
||||
if (!content) continue;
|
||||
|
||||
const nodesInFile = ctx.getNodesInFile(file);
|
||||
const lineOf = (idx: number) => content.slice(0, idx).split('\n').length;
|
||||
const lineOf = makeLineAt(content, 1);
|
||||
const addDispatcher = (event: string, line: number) => {
|
||||
const disp = enclosingFn(nodesInFile, line);
|
||||
if (!disp) return;
|
||||
|
||||
Reference in New Issue
Block a user