mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
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:
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user