mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
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:
1 parent
a9195c3a98
commit
0c6c571a01
2 files changed
+130
-20
No files matched your search
@@ -54,6 +54,57 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
const selections = new Map<string, readonly string[]>();
|
||||
const coveringPaths = new Map<string, SemanticPropertyPathCoverage>();
|
||||
let arrayIterationUnknown = false;
|
||||
// Definition collection finishes before this phase, so a scope's bindings no
|
||||
// longer change and one node resolves to one binding. Repeated visits of the
|
||||
// same node otherwise repeat the same scope walk.
|
||||
const resolvedBindings = new WeakMap<
|
||||
t.Node,
|
||||
JavaScriptSemanticBindingState | null
|
||||
>();
|
||||
const resolveBinding = (
|
||||
node: t.Node,
|
||||
name: string,
|
||||
): JavaScriptSemanticBindingState | undefined => {
|
||||
const cached = resolvedBindings.get(node);
|
||||
if (cached !== undefined) return cached ?? undefined;
|
||||
const resolved = resolveSemanticBindingState(state, node, name);
|
||||
resolvedBindings.set(node, resolved ?? null);
|
||||
return resolved;
|
||||
};
|
||||
// One capture tuple recurs across every alias it reaches. Re-encoding it
|
||||
// would rebuild the same long key and force a fresh string hash per lookup.
|
||||
const captureKeys = new WeakMap<
|
||||
JavaScriptSemanticBindingState,
|
||||
Map<string, string>
|
||||
>();
|
||||
const captureKeyOf = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
originAt: t.Node | undefined,
|
||||
ownerInitializer: t.Node | undefined,
|
||||
): string => {
|
||||
let byPosition = captureKeys.get(binding);
|
||||
if (byPosition === undefined) {
|
||||
byPosition = new Map();
|
||||
captureKeys.set(binding, byPosition);
|
||||
}
|
||||
const positionKey = `${originAt?.start ?? "u"}:${originAt?.end ?? "u"}:${
|
||||
ownerInitializer?.start ?? "u"
|
||||
}:${ownerInitializer?.end ?? "u"}`;
|
||||
const cached = byPosition.get(positionKey);
|
||||
if (cached !== undefined) return cached;
|
||||
const created = JSON.stringify([
|
||||
binding.bindingId,
|
||||
originAt?.start,
|
||||
originAt?.end,
|
||||
ownerInitializer?.start,
|
||||
ownerInitializer?.end,
|
||||
]);
|
||||
byPosition.set(positionKey, created);
|
||||
return created;
|
||||
};
|
||||
// A capture key already ends the array; a path stays one nested element.
|
||||
const lifetimeKey = (captureKey: string, path: PropertyPath): string =>
|
||||
`${captureKey.slice(0, -1)},${JSON.stringify(path)}]`;
|
||||
const pendingReferences: {
|
||||
readonly initializer: JavaScriptSemanticBindingState["referenceInitializers"][number];
|
||||
readonly path: PropertyPath;
|
||||
@@ -121,11 +172,7 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
if (current === undefined) break;
|
||||
const expression = unwrapJavaScriptExpression(current.node).node;
|
||||
if (t.isIdentifier(expression)) {
|
||||
const binding = resolveSemanticBindingState(
|
||||
state,
|
||||
expression,
|
||||
expression.name,
|
||||
);
|
||||
const binding = resolveBinding(expression, expression.name);
|
||||
if (binding === undefined) {
|
||||
if (
|
||||
(expression.name === "Array" &&
|
||||
@@ -142,21 +189,18 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
// An alias restored from a saved reference can revisit the same binding
|
||||
// at an earlier capture or a shorter getter-receiver path. Only a
|
||||
// repeated capture and path closes a cycle.
|
||||
const capture = [
|
||||
binding.bindingId,
|
||||
current.originAt?.start,
|
||||
current.originAt?.end,
|
||||
current.ownerInitializer?.start,
|
||||
current.ownerInitializer?.end,
|
||||
];
|
||||
const lifetime = JSON.stringify([...capture, current.path]);
|
||||
const captureKey = captureKeyOf(
|
||||
binding,
|
||||
current.originAt,
|
||||
current.ownerInitializer,
|
||||
);
|
||||
const lifetime = lifetimeKey(captureKey, current.path);
|
||||
if (current.bindings.has(lifetime)) continue;
|
||||
// A self alias such as `node = node[key]` revisits its capture with the
|
||||
// same prefix added on every cycle, so exact paths never repeat. When
|
||||
// that prefix can never be pruned, escaping it once covers every longer
|
||||
// effect, including writes below shared children.
|
||||
// Only a capture already on this alias chain can repeat a prefix.
|
||||
const captureKey = JSON.stringify(capture);
|
||||
const revisited = current.bindings.has(captureKey);
|
||||
let repeatedPrefix: PropertyPath | undefined;
|
||||
for (
|
||||
@@ -165,8 +209,7 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
start++
|
||||
) {
|
||||
const suffix = current.path.slice(start);
|
||||
if (!current.bindings.has(JSON.stringify([...capture, suffix])))
|
||||
continue;
|
||||
if (!current.bindings.has(lifetimeKey(captureKey, suffix))) continue;
|
||||
repeatedPrefix = current.path.slice(0, start);
|
||||
break;
|
||||
}
|
||||
@@ -408,7 +451,7 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
if (!t.isNode(node)) continue;
|
||||
const pathKey = JSON.stringify(value.path);
|
||||
if (t.isIdentifier(node) && value.iterableFallbackPath === undefined) {
|
||||
const binding = resolveSemanticBindingState(state, node, node.name);
|
||||
const binding = resolveBinding(node, node.name);
|
||||
const identity = JSON.stringify([
|
||||
binding === undefined ? null : binding.bindingId,
|
||||
binding === undefined ? node.name : null,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user