From 1ec490f36180cf6d3a8f353e9e43920dbd81ece0 Mon Sep 17 00:00:00 2001 From: Zeynel <137368801+zeynelacikgoez@users.noreply.github.com> Date: Sat, 10 Oct 2026 08:04:31 +0200 Subject: [PATCH] fix(javascript): stop replaying earlier writes for each object property write Repeated property writes to one object made member-mutation collection cubic. Each write evaluated its target binding to rule out a primitive, and every evaluation folded all earlier mutation paths, copying every slot once per path. 2,000 writes to one object took 27 s and 4,000 took 203 s. - A recorded write always leaves a non-primitive binding value, so only the first write to a binding needs the primitive check. - Apply mutation paths per slot in one pass with the same ordering semantics instead of one full slot copy per path. Co-authored-by: Claude Opus 5.5 Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> --- ...tSemanticAnalysis.member_mutations.test.ts | 21 ++++ .../javascriptSemanticMemberMutations.ts | 5 +- .../javascriptSemanticMutationValues.ts | 118 ++++++++++++------ .../javascript/javascriptSemanticValues.ts | 10 +- 4 files changed, 106 insertions(+), 48 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts index f28436eb1d..3450060cbe 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts @@ -590,3 +590,24 @@ describe("property mutations through TypeScript satisfies aliases", () => { ).toEqual({ status: "literal", value: "TOKEN" }); }); }); + +describe("property mutation collection on minified bundles", () => { + it("records each write to one object without replaying earlier writes", () => { + const writes = Array.from( + { length: 2000 }, + (_, index) => `source.p${index} = ${index};`, + ).join(" "); + const start = performance.now(); + const value = resultValue( + `const source = { token: "TOKEN" }; ${writes} return [source.token, source.p1999];`, + ); + expect(performance.now() - start).toBeLessThan(2000); + expect(value).toMatchObject({ + status: "array", + items: [ + { value: { status: "literal", value: "TOKEN" } }, + { value: { status: "unknown" } }, + ], + }); + }, 30000); +}); diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index 4e4ea7b8b2..1414d407f0 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -194,8 +194,11 @@ export const collectSemanticMemberMutations = ( current.originAt?.end, ]); if (recordedEffects.has(identity)) continue; + // A recorded write invalidates the binding to a non-primitive value, so + // only its first write can target a primitive. Re-evaluating it would + // replay every earlier write and make repeated writes superlinear. const value = - effect === "write" + effect === "write" && binding.mutatedPaths.length === 0 ? evaluateSemanticBinding(binding, state) : undefined; const primitiveWrite = diff --git a/src/domain/javascript/javascriptSemanticMutationValues.ts b/src/domain/javascript/javascriptSemanticMutationValues.ts index dbbc9d4572..bc2213da7b 100644 --- a/src/domain/javascript/javascriptSemanticMutationValues.ts +++ b/src/domain/javascript/javascriptSemanticMutationValues.ts @@ -33,49 +33,89 @@ export const invalidateSemanticEscapedPath = ( : { ...value, items: value.items.map(invalidateChild) }; }; -/** Invalidate only slots that an explicit mutation can affect. */ -export const invalidateSemanticMutationPath = ( +/** + * Invalidate only slots that explicit mutations can affect, as if applying each + * path in order. Paths are grouped per slot so that many writes to one object + * cost one pass over its slots rather than one pass per write. + */ +export const invalidateSemanticMutationPaths = ( value: JavaScriptSemanticValue, - path: JavaScriptSemanticPropertyPath, + paths: readonly JavaScriptSemanticPropertyPath[], ): JavaScriptSemanticValue => { - const [key, ...remaining] = path; - const unknown: JavaScriptSemanticValue = { - status: "unknown", - reason: "This property may have been mutated.", + if (paths.length === 0) return value; + // An unknown key or a replaced value leaves nothing that a later path can + // restore, wherever it occurs in the sequence. + if ( + (value.status !== "object" && value.status !== "array") || + paths.some(([key]) => key === undefined || key === null) + ) + return { + status: "unknown", + reason: "This property may have been mutated.", + }; + // Writing an array length discards every item known before it. + const reset = + value.status === "array" + ? paths.findLastIndex(([key]) => key === "length") + : -1; + const base: typeof value = + reset < 0 + ? value + : { status: "array", items: [], unknownItems: true, omittedItems: null }; + const slots = base.status === "object" ? base.properties : base.items; + const slotsByName = new Map(); + slots.forEach((property, index) => { + const indexes = slotsByName.get(property.name) ?? []; + indexes.push(index); + slotsByName.set(property.name, indexes); + }); + const slotPaths = new Map(); + const replacedSlots = new Set(); + const select = (index: number, remaining: JavaScriptSemanticPropertyPath) => { + const selected = slotPaths.get(index) ?? []; + selected.push(remaining); + slotPaths.set(index, selected); + if (remaining.length === 0) replacedSlots.add(index); }; - if (key === undefined || key === null) return unknown; - if (value.status === "object" || value.status === "array") { - if (value.status === "array" && key === "length") - return { - status: "array", - items: [], - unknownItems: true, - omittedItems: null, - }; - const slots = value.status === "object" ? value.properties : value.items; - const observed = slots.some((property) => - semanticPropertyPathKeyMatches(key, property.name), - ); - const properties = slots.map((property) => - semanticPropertyPathKeyMatches(key, property.name) - ? { + // An added slot is already unknown, so later paths that select it are inert. + const added = new Set(); + for (const [key, ...remaining] of paths.slice(reset + 1)) { + if (key === undefined || key === null) continue; + if (typeof key === "object") { + slots.forEach((property, index) => { + if (semanticPropertyPathKeyMatches(key, property.name)) + select(index, remaining); + }); + continue; + } + const indexes = slotsByName.get(String(key)); + if (indexes === undefined) added.add(String(key)); + else for (const index of indexes) select(index, remaining); + } + const properties: JavaScriptSemanticProperty[] = slots.map( + (property, index) => { + const selected = slotPaths.get(index); + return selected === undefined + ? property + : { ...property, - ...(remaining.length === 0 + ...(replacedSlots.has(index) ? { presence: "unknown-coverage" as const } : {}), - value: invalidateSemanticMutationPath(property.value, remaining), - } - : property, - ); - if (!observed && typeof key !== "object") - properties.push({ - name: String(key), - value: unknown, - presence: "unknown-coverage", - }); - return value.status === "object" - ? { ...value, properties } - : { ...value, items: properties }; - } - return unknown; + value: invalidateSemanticMutationPaths(property.value, selected), + }; + }, + ); + for (const name of added) + properties.push({ + name, + value: { + status: "unknown", + reason: "This property may have been mutated.", + }, + presence: "unknown-coverage", + }); + return base.status === "object" + ? { ...base, properties } + : { ...base, items: properties }; }; diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index 884915948c..87f6ad540e 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -10,7 +10,7 @@ import { semanticSlotAtPath } from "./javascriptSemanticSlots.js"; import { invalidateSemanticEscapedPath, - invalidateSemanticMutationPath, + invalidateSemanticMutationPaths, } from "./javascriptSemanticMutationValues.js"; import type { JavaScriptBindingProvenance } from "./javascriptSemanticIr.js"; @@ -211,13 +211,7 @@ const evaluateBinding = ( if (isPrimitive(captured)) value = captured; } } - let projected = value; - for (const path of binding.mutatedPaths) { - projected = invalidateSemanticMutationPath(projected, path); - // Every subsequent write returns this same root unknown. Replaying those - // paths at every newly collected effect otherwise becomes quadratic. - if (projected.status === "unknown") break; - } + let projected = invalidateSemanticMutationPaths(value, binding.mutatedPaths); for (const escape of binding.escapedPaths) { // Escapes only invalidate object references; other lattice values survive. if (projected.status !== "object" && projected.status !== "array") break;