perf(javascript): prune typed-array element reference escapes (#5139)

* perf(javascript): prune typed-array element reference escapes

* fix(javascript): retain references from typed-array element properties

---------

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ge Li
2026-10-22 17:41:50 -04:00
committed by GitHub
parent 0344d578b2
commit 6d73515bf1
3 changed files with 410 additions and 10 deletions
@@ -0,0 +1,289 @@
import { describe, expect, it } from "vitest";
import {
analyzeJavaScriptSemantics,
indexJavaScriptBindingsWithReceiversSteps,
} from "./javascriptSemanticAnalysis.js";
import { onlyCallable } from "./javascriptSemanticAnalysis.fixture.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
const mutationBindings = (source: string) => {
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("Failed to parse fixture");
expect(parsed.errors).toEqual([]);
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
);
completeSemanticSteps(index.indexMemberMutationFactsSteps());
return [...index.bindings.bindingsById.values()];
};
const escapes = (source: string, name = "value") => {
const matches = mutationBindings(source).filter(
(binding) => binding.name === name,
);
expect(matches).toHaveLength(1);
return matches[0]!.escapedPaths;
};
describe("integer-indexed values in reference propagation", () => {
it.each([
"Int8Array",
"Uint8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"Float16Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
"globalThis.Int32Array",
])(
"does not retain reference effects for %s element values",
(constructor) => {
expect(
escapes(`
const heap = new ${constructor}(16);
const value = heap[index >> 2];
consume(value);
`),
).toEqual([]);
},
);
it.each([
"0",
"-0",
"-1",
"1.5",
"1e999",
'"NaN"',
'"Infinity"',
'"-0"',
'"1"',
"index >>> shift",
"index >> 2",
"index & 255",
"+index",
"index / 2",
])("recognizes Number and canonical numeric keys: %s", (key) => {
expect(
escapes(
`const heap = new Int32Array(16); const value = heap[${key}]; consume(value);`,
),
).toEqual([]);
});
it.each([
'"01"',
'"1.0"',
'" 1"',
'"+1"',
'"extra"',
'"buffer"',
'"__proto__"',
"key",
"index + 1",
"index >> shift",
"9007199254740993n",
])(
"retains possible references for an ordinary or unknown property: %s",
(key) => {
expect(
escapes(
`const heap = new Int32Array(16); const value = heap[${key}]; consume(value);`,
).length,
).toBeGreaterThan(0);
},
);
it.each([
"const Int32Array = Factory;",
"globalThis.Int32Array = Factory;",
"replace(globalThis);",
"(0, eval)(source);",
"eval(source);",
])("keeps uncertain constructor identities conservative: %s", (prefix) => {
expect(
escapes(
`${prefix} const heap = new Int32Array(16); const value = heap[index >> 2]; consume(value);`,
).length,
).toBeGreaterThan(0);
});
});
describe("references reached through typed-array element properties", () => {
it.each([
["Int32Array", "0", "constructor"],
["BigInt64Array", "0", "constructor"],
["Int32Array", "index >> 2", "toString"],
["BigInt64Array", "index >> 2", "toString"],
["Int32Array", "0", "customReference"],
["BigInt64Array", "0", "customReference"],
])("retains %s[%s].%s references", (constructor, key, property) => {
expect(
escapes(`
const heap = new ${constructor}(16);
const value = heap[${key}];
consume(value.${property});
`).length,
).toBeGreaterThan(0);
});
it.each(["Int32Array", "BigInt64Array"])(
"retains unknown property reads from %s elements",
(constructor) => {
expect(
escapes(`
const heap = new ${constructor}(16);
const value = heap[index >> 2];
consume(value[property]);
`).length,
).toBeGreaterThan(0);
},
);
it.each(["Int32Array", "BigInt64Array"])(
"retains nested property reads through %s element aliases",
(constructor) => {
expect(
escapes(`
const heap = new ${constructor}(16);
const element = heap[index >> 2];
const value = element;
consume(value.constructor.prototype);
`).length,
).toBeGreaterThan(0);
},
);
});
describe("typed-array reference dependencies", () => {
it("propagates numeric-element facts through aliases and buffer replacement", () => {
expect(
escapes(`
let heap = new Int32Array(buffer);
function grow(next) { heap = new Int32Array(next); }
const alias = heap;
let value = alias[index >> 2];
value = flag ? heap[4] : alias[index >>> 0];
consume(value);
`),
).toEqual([]);
});
it("does not lose a reference mixed with an integer-indexed value", () => {
const ir = analyzeJavaScriptSemantics(`
export function result() {
const child = { value: 1 };
const heap = flag ? new Int32Array(16) : [child];
const value = heap[0];
consume(value);
return child.value;
}
`);
expect(onlyCallable(ir, "result").returnSites[0]?.value.status).toBe(
"unknown",
);
});
it("keeps distinct numeric slots of ordinary arrays separate", () => {
const ir = analyzeJavaScriptSemantics(`
export function result() {
const child = { value: 1 };
const array = [0, child];
const zero = array[0];
const value = array[1];
consume(value);
return child.value;
}
`);
expect(onlyCallable(ir, "result").returnSites[0]?.value.status).toBe(
"unknown",
);
});
it("keeps unknown and numeric projections distinct on the same view", () => {
expect(
escapes(`
const heap = new Int32Array(16);
const scalar = heap[index >> 2];
const value = heap[key];
consume(value);
`).length,
).toBeGreaterThan(0);
});
it.each([
"function result(Int32Array) {",
"function result(globalThis) { const Int32Array = globalThis.Int32Array;",
])("does not assume a shadowed constructor is intrinsic: %s", (prefix) => {
expect(
escapes(`${prefix}
const heap = new Int32Array(16);
const value = heap[0];
consume(value);
}`).length,
).toBeGreaterThan(0);
});
it.each(["new Proxy(new Int32Array(16), handler)", "new CustomArray(16)"])(
"retains references from an uncertain receiver: %s",
(receiver) => {
expect(
escapes(
`const heap = ${receiver}; const value = heap[0]; consume(value);`,
).length,
).toBeGreaterThan(0);
},
);
it("retains references when a view is reassigned an ordinary array", () => {
const ir = analyzeJavaScriptSemantics(`
export function result() {
const child = { value: 1 };
let heap = new Int32Array(16);
heap = [child];
const value = heap[0];
consume(value);
return child.value;
}
`);
expect(onlyCallable(ir, "result").returnSites[0]?.value.status).toBe(
"unknown",
);
});
it("retains escapes of the view itself and writes to its elements", () => {
const bindings = mutationBindings(`
const heap = new Int32Array(16);
heap[index >> 2] = 5;
consume(heap);
`);
const heap = bindings.find((binding) => binding.name === "heap")!;
expect(heap.memberWrites.length).toBeGreaterThan(0);
expect(heap.escapedPaths.length).toBeGreaterThan(0);
});
it("does not retain the cross-product of scalar assignments and store sites", () => {
const stores = Array.from(
{ length: 48 },
(_, i) =>
`value = heap[index + ${i} >> 2]; heap[index + ${i + 1} >> 2] = value;`,
).join("\n");
const bindings = mutationBindings(`
const heap = new Int32Array(buffer);
function copy(index) { let value = 0; ${stores} }
`);
expect(
bindings.reduce((n, binding) => n + binding.escapedPaths.length, 0),
).toBe(0);
expect(
bindings.find((binding) => binding.name === "heap")!.memberWrites.length,
).toBeGreaterThan(0);
});
});
@@ -1,5 +1,9 @@
import * as t from "@babel/types";
import { isIntrinsicMapLookup } from "./javascriptSemanticMapOperations.js";
import {
isIntrinsicTypedArrayConstruction,
isTypedArrayNumericIndex,
} from "./javascriptSemanticTypedArrays.js";
import {
semanticGlobalProcessOrigin,
@@ -1218,6 +1222,8 @@ type MutationObservableOrigin =
| "ambient"
| "array"
| "array-prototype"
| "typed-array"
| "typed-array-element"
| "function"
| "opaque";
@@ -1229,6 +1235,7 @@ interface MutationObserverNode {
readonly read?: {
readonly source: MutationObserverNode;
readonly key: PropertyPath[number];
readonly numericIndex: boolean;
};
}
@@ -1283,15 +1290,16 @@ const observableMutationPaths = (
const project = (
source: MutationObserverNode,
path: PropertyPath,
numericIndex = false,
): MutationObserverNode => {
let observer = source;
for (const key of path) {
const identity = JSON.stringify(
const identity = `${numericIndex ? "numeric:" : ""}${JSON.stringify(
key === null || typeof key === "object" ? key : String(key),
);
)}`;
let selected = observer.projections?.get(identity);
if (selected === undefined) {
selected = create({ source: observer, key });
selected = create({ source: observer, key, numericIndex });
(observer.projections ??= new Map()).set(identity, selected);
(observer.dependents ??= new Set()).add(selected);
schedule(selected);
@@ -1352,6 +1360,11 @@ const observableMutationPaths = (
// Keep class/getter projections distinct so cycle pruning does not
// discard observable escapes through a shared getter result.
if (
t.isNewExpression(expression) &&
isIntrinsicTypedArrayConstruction(expression, state)
)
(observer.origins ??= new Set()).add("typed-array");
else if (
t.isNewExpression(expression) &&
semanticClassCandidates(expression.callee, state).length === 0
)
@@ -1383,12 +1396,19 @@ const observableMutationPaths = (
t.isOptionalMemberExpression(expression)
)
link(
project(expressionNode(expression.object), [
semanticStaticPropertyKey(
project(
expressionNode(expression.object),
[
semanticStaticPropertyKey(
expression.property,
expression.computed,
),
],
isTypedArrayNumericIndex(
expression.property,
expression.computed,
),
]),
),
observer,
);
else
@@ -1416,16 +1436,29 @@ const observableMutationPaths = (
for (const source of observer.sources ?? [])
for (const origin of source.origins ?? []) add(origin);
if (observer.read !== undefined) {
const { source, key } = observer.read;
const { source, key, numericIndex } = observer.read;
if (key === null || typeof key === "object") {
// Dynamic keys and copied-slot masks retain their original paths.
// Enumerating their exclusion combinations would recreate the growth
// that the mutation collector's selection union already prevents.
if ((source.origins?.size ?? 0) > 0) add("opaque");
for (const origin of source.origins ?? [])
add(
origin === "typed-array" && numericIndex
? "typed-array-element"
: "opaque",
);
} else
for (const origin of source.origins ?? []) {
if (typeof origin === "string") {
if (origin === "array-prototype" || origin === "opaque")
if (origin === "typed-array") {
// Custom properties can hold references; only canonical numeric
// indices have the native Number/BigInt/undefined result contract.
add(numericIndex ? "typed-array-element" : "opaque");
} else if (origin === "typed-array-element") {
// Number/BigInt property reads can expose constructors, methods
// or objects returned by prototype getters after boxing.
add("opaque");
} else if (origin === "array-prototype" || origin === "opaque")
add(origin);
else if (
origin === "ambient" &&
@@ -1477,7 +1510,12 @@ const observableMutationPaths = (
if (effect === "escape") {
observer = project(observer, path);
settle();
return (observer.origins?.size ?? 0) === 0 ? null : path;
const origins = observer.origins;
return origins === undefined ||
origins.size === 0 ||
(origins.size === 1 && origins.has("typed-array-element"))
? null
: path;
}
if ((observer.origins?.size ?? 0) === 0)
return path.length === 0 ? path : [null];
@@ -0,0 +1,73 @@
import * as t from "@babel/types";
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
import { semanticOpaqueGlobalMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js";
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
const typedArrayConstructors = [
"Int8Array",
"Uint8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"Float16Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
];
/** Recognize native integer-indexed allocations, not constructor spelling alone. */
export const isIntrinsicTypedArrayConstruction = (
node: t.NewExpression,
state: JavaScriptSemanticAnalysisState,
): boolean =>
!semanticOpaqueGlobalMayHaveChanged(node, state) &&
typedArrayConstructors.some(
(name) =>
isUnmodifiedGlobalReference(node.callee, state, name) &&
!state.ambientEscapes.covers(["global", name]),
);
/** A successful evaluation of these expressions produces a Number, not a key object. */
const hasNumberResult = (node: t.Node): boolean => {
const pending = [node];
while (pending.length > 0) {
const expression = unwrapJavaScriptExpression(pending.pop()!).node;
if (t.isNumericLiteral(expression)) return true;
if (t.isUnaryExpression(expression)) {
if (expression.operator === "+") return true;
if (expression.operator === "-" || expression.operator === "~")
pending.push(expression.argument);
} else if (t.isBinaryExpression(expression)) {
if (expression.operator === ">>>") return true;
// These operators reject mixed Number/BigInt operands. Either Number
// operand proves a Number result on success; + can concatenate strings.
if (
["-", "*", "/", "%", "**", "<<", ">>", "&", "|", "^"].includes(
expression.operator,
)
)
pending.push(expression.left, expression.right);
}
}
return false;
};
/** Numeric indices never expose an object, even out of bounds or on a detached view. */
export const isTypedArrayNumericIndex = (
key: t.Node,
computed: boolean,
): boolean => {
if (!computed) return false;
const expression = unwrapJavaScriptExpression(key).node;
if (t.isStringLiteral(expression))
return (
expression.value === "-0" ||
String(Number(expression.value)) === expression.value
);
return hasNumberResult(expression);
};