perf(javascript): reuse semantic member-mutation lookups (#2941)

The semantic member-mutation collector revisits the same nodes while it closes
alias chains, and each visit re-derived results that cannot change within the
phase:

- resolveSemanticBindingState re-walked the scope chain for the same identifier
  node. Definition collection completes before this phase, and addBinding is
  reachable only from it, so a scope's bindings no longer change.
- directInitializerPosition, directMutationPosition and
  referenceInitializerPosition are pure functions of a node and the analysis
  parent map, but were recomputed for every mutation and initializer.
- markValue re-encoded the same capture tuple with JSON.stringify and built a
  fresh long key for every Set/Map lookup, so each lookup re-hashed the whole
  key. The capture key is now cached per binding, origin and owner, and the
  lifetime key is derived from it.

On pdfjs-dist 5.3.93 wasm/openjpeg_nowasm_fallback.js (462316 bytes, sha256
53c228428a8861035a34738966af263fc9d4cee6df25a0ad942af947469a0bd9) the semantic
phase takes 177.8 s instead of 272.9 s in-process, with a byte-identical
semantic IR. With --max-heap-mb 3072 and the default heap the base revision
exhausts the worker; with the default 300 s per-source deadline raised, the
completed CLI run takes 240.6 s instead of 396.7 s and its output is identical
apart from volatile identifiers.

Refs #2498

Co-authored-by: gongfpp <22683417+gongfpp@users.noreply.github.com>
This commit is contained in:
gongfppandgongfpp authored and GitHub committed 2026-10-14 03:09:34 +08:00
1 parent a9195c3a98
commit 0c6c571a01
2 files changed
+130 -20

No files matched your search

@@ -12,6 +12,37 @@ interface StatementPosition {
readonly declaratorIndex: number;
}
/**
* Initializer positions depend only on a node and its parent map, which one
* analysis builds once and never changes. Different mutations and initializers
* re-derive the same positions along shared ancestry, so retain the first
* result for as long as its parent map is reachable.
*/
interface InitializerPositionCache {
readonly direct: WeakMap<t.Node, StatementPosition | null>;
readonly mutation: WeakMap<t.Node, StatementPosition | null>;
readonly reference: WeakMap<t.Node, StatementPosition | null>;
}
const initializerPositionCaches = new WeakMap<
WeakMap<t.Node, t.Node>,
InitializerPositionCache
>();
const initializerPositionCache = (
parents: WeakMap<t.Node, t.Node>,
): InitializerPositionCache => {
const cached = initializerPositionCaches.get(parents);
if (cached !== undefined) return cached;
const created: InitializerPositionCache = {
direct: new WeakMap(),
mutation: new WeakMap(),
reference: new WeakMap(),
};
initializerPositionCaches.set(parents, created);
return created;
};
const statementPosition = (
statement: t.Statement,
parents: WeakMap<t.Node, t.Node>,
@@ -42,7 +73,7 @@ const positionPrecedes = (
left.declaratorIndex >= 0 &&
left.declaratorIndex < right.declaratorIndex));
const directInitializerPosition = (
const computeDirectInitializerPosition = (
node: t.Node,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
@@ -67,7 +98,19 @@ const directInitializerPosition = (
return null;
};
const directMutationPosition = (
const directInitializerPosition = (
node: t.Node,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
const cache = initializerPositionCache(parents);
const cached = cache.direct.get(node);
if (cached !== undefined) return cached;
const computed = computeDirectInitializerPosition(node, parents);
cache.direct.set(node, computed);
return computed;
};
const computeDirectMutationPosition = (
node: t.Node,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
@@ -115,7 +158,19 @@ const directMutationPosition = (
}
};
const referenceInitializerPosition = (
const directMutationPosition = (
node: t.Node,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
const cache = initializerPositionCache(parents);
const cached = cache.mutation.get(node);
if (cached !== undefined) return cached;
const computed = computeDirectMutationPosition(node, parents);
cache.mutation.set(node, computed);
return computed;
};
const computeReferenceInitializerPosition = (
node: t.Node,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
@@ -137,6 +192,18 @@ const referenceInitializerPosition = (
}
};
const referenceInitializerPosition = (
node: t.Node,
parents: WeakMap<t.Node, t.Node>,
): StatementPosition | null => {
const cache = initializerPositionCache(parents);
const cached = cache.reference.get(node);
if (cached !== undefined) return cached;
const computed = computeReferenceInitializerPosition(node, parents);
cache.reference.set(node, computed);
return computed;
};
/** Resolve origins at their use or capture, never at a later transitive effect. */
export const semanticMutationInitializers = (
binding: JavaScriptSemanticBindingState,