[PR #1444] [MERGED] fix(javascript): stop replaying earlier writes for each object property write #2891

Closed
opened 2026-10-17 21:11:28 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/1444
Author: @zeynelacikgoez
Created: 10/10/2026
Status: ✅ Merged
Merged: 10/10/2026
Merged by: @N0zoM1z0

Base: main ← Head: fix/javascript-repeated-write-complexity


📝 Commits (3)

  • 1b1408a fix(javascript): stop replaying earlier writes for each object property write
  • 4964eeb chore: merge main into repeated-write performance fix
  • 439bc1a chore: include current slot-store fixes in performance validation

📊 Changes

4 files changed (+106 additions, -48 deletions)

View changed files

📝 src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts (+21 -0)
📝 src/domain/javascript/javascriptSemanticMemberMutations.ts (+4 -1)
📝 src/domain/javascript/javascriptSemanticMutationValues.ts (+79 -39)
📝 src/domain/javascript/javascriptSemanticValues.ts (+2 -8)

📄 Description

Summary

Member-mutation collection is still cubic in the number of property writes to a single object on current main. CommonJS/webpack exports objects and minified configuration builders make this pattern common in real bundles. On main (0b9815b6), analyzeJavaScriptSemantics takes 27 s for 2,000 writes to one object and 203 s for 4,000. With this change, those take 0.09 s and 0.16 s.

This is separate from #1401, which 28595276 fixed (factorial growth through reassignment loops). That fix bounds path enumeration. The cost here is in evaluating the written binding.

Problem and expected behavior

Smallest trigger:

export function result() {
  const source = { token: "TOKEN" };
  source.p0 = 0; source.p1 = 1; /* … */ source.p1999 = 1999;
  return [source.token, source.p1999];
}

There are two compounding costs in collectSemanticMemberMutations and evaluateBinding:

  1. Per-write evaluation.
    • Each recorded write calls evaluateSemanticBinding on its target, only to rule out a primitive target, because writes to primitives are ignored.
    • Non-primitive results are not memoized, and each recorded write clears the primitive cache.
    • So the k-th write rebuilds the initializer and replays all k − 1 earlier mutation paths.
  2. Per-path slot copies. Folding binding.mutatedPaths with invalidateSemanticMutationPath copies the whole slot array once per path, so one evaluation costs O(k · slots).

Together that is O(k²·(slots + k)). Expected behavior: cost proportional to the writes, with identical values.

Change and scope

  • First write only (javascriptSemanticMemberMutations.ts).
    • invalidateSemanticMutationPath returns either an object/array or unknown, never a literal/union.
    • So once a binding has one recorded write, it cannot evaluate to a primitive, and the check is needed only for its first write.
    • Capture-point evaluations use separate caches and are unaffected.
  • Single-pass fold (javascriptSemanticMutationValues.ts, javascriptSemanticValues.ts).
    • invalidateSemanticMutationPaths(value, paths) groups paths per slot and recurses once per slot.
    • It reproduces the sequential order-sensitive rules exactly:
      • any null/empty key ⇒ the same root unknown, which subsumes the existing early break;
      • the last array length write resets items;
      • an unmatched exact key appends one unknown-coverage slot;
      • later paths into an added slot are inert;
      • selectors still test every slot.
    • It replaces the single-path function, which had no other callers.

Not included: no caps or truncation, no change to cycle handling from 28595276, and no contract, schema, or catalog change.

Contract and boundary impact

  • Semantic owner and earliest changed stage: src/domain/javascript member-mutation collection and binding evaluation.
  • CLI and MCP/tool-catalog contract: none.
  • Provider, bridge, target-format, or platform compatibility: none.
  • Evidence, artifact, provenance, or reconstruction contract: none. Values are identical (see below).
  • Process execution, authorization, cleanup, or containment impact: none.
  • Generated metadata, package, or installation impact: none.

Evidence and regression coverage

  • Test added: javascriptSemanticAnalysis.member_mutations.test.ts › "records each write to one object without replaying earlier writes".

    • It writes 2,000 properties to one object, then asserts that the untouched slot stays literal and the written slot is unknown, within 2 s.
    • On the base revision it fails after 43 s (expected 42984 to be less than 2000).
  • Base reproduction on Node 24.18.0 / macOS arm64:

    Writes to one object main 0b9815b6 this change
    1,000 3.4 s 0.07 s
    2,000 27.4 s 0.09 s
    4,000 202.9 s 0.16 s
    8,000 — 0.29 s
    • Notion Desktop 7.37.1 bundles: .webpack/main/index.js (4.2 MB) 10.6 s → 8.7 s; .webpack/renderer/activity_monitor/index.js 0.98 s → 0.95 s.
  • Value equivalence:

    • Across all 96 JavaScript files in Notion Desktop 7.37.1's app.asar, all 58,900 binding and return-site values are byte-identical between main and this change.
    • 200,000 randomized value and path sequences (selectors, null keys, length, duplicate names, nesting) fold identically to the sequential per-path reduce.
  • Commands run:

    • npm run check:fast, which passes.
    • npm run test:local -- src/domain/javascript/*.test.ts src/application/javascript/*.test.ts: 55 files, 989 tests, passing.
    • npm run test:focused -- with the 42 JavaScript CLI/MCP/composition test files: 286/290 passed. All 4 failures were 30 s timeouts in tests/acceptance/applications/exportedFunctionBindingsCli.test.ts and tests/acceptance/tools/localToolContracts.test.ts. Run alone, those two files also time out on unmodified main on this machine (5 failures there vs 3 with this change).
  • Real Hopper/Ghidra verification: not applicable; JavaScript-only change.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/1444 **Author:** [@zeynelacikgoez](https://github.com/zeynelacikgoez) **Created:** 10/10/2026 **Status:** ✅ Merged **Merged:** 10/10/2026 **Merged by:** [@N0zoM1z0](https://github.com/N0zoM1z0) **Base:** `main` ← **Head:** `fix/javascript-repeated-write-complexity` --- ### 📝 Commits (3) - [`1b1408a`](https://github.com/morluto/rea/commit/1b1408a0b169a0fa1118cdca6722364f0ddd3a35) fix(javascript): stop replaying earlier writes for each object property write - [`4964eeb`](https://github.com/morluto/rea/commit/4964eeb6e015165842ca6dd5899909f8bb83a299) chore: merge main into repeated-write performance fix - [`439bc1a`](https://github.com/morluto/rea/commit/439bc1a5d06f6db042161922ea8b1662ffc933a9) chore: include current slot-store fixes in performance validation ### 📊 Changes **4 files changed** (+106 additions, -48 deletions) <details> <summary>View changed files</summary> 📝 `src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts` (+21 -0) 📝 `src/domain/javascript/javascriptSemanticMemberMutations.ts` (+4 -1) 📝 `src/domain/javascript/javascriptSemanticMutationValues.ts` (+79 -39) 📝 `src/domain/javascript/javascriptSemanticValues.ts` (+2 -8) </details> ### 📄 Description ## Summary Member-mutation collection is still cubic in the number of property writes to a single object on current `main`. CommonJS/webpack exports objects and minified configuration builders make this pattern common in real bundles. On `main` (`0b9815b6`), `analyzeJavaScriptSemantics` takes 27 s for 2,000 writes to one object and 203 s for 4,000. With this change, those take 0.09 s and 0.16 s. This is separate from #1401, which `28595276` fixed (factorial growth through reassignment loops). That fix bounds path *enumeration*. The cost here is in *evaluating* the written binding. ## Problem and expected behavior Smallest trigger: ```js export function result() { const source = { token: "TOKEN" }; source.p0 = 0; source.p1 = 1; /* … */ source.p1999 = 1999; return [source.token, source.p1999]; } ``` There are two compounding costs in `collectSemanticMemberMutations` and `evaluateBinding`: 1. **Per-write evaluation.** - Each recorded write calls `evaluateSemanticBinding` on its target, only to rule out a primitive target, because writes to primitives are ignored. - Non-primitive results are not memoized, and each recorded write clears the primitive cache. - So the k-th write rebuilds the initializer and replays all k − 1 earlier mutation paths. 2. **Per-path slot copies.** Folding `binding.mutatedPaths` with `invalidateSemanticMutationPath` copies the whole slot array once per path, so one evaluation costs O(k · slots). Together that is O(k²·(slots + k)). Expected behavior: cost proportional to the writes, with identical values. ## Change and scope - **First write only** (`javascriptSemanticMemberMutations.ts`). - `invalidateSemanticMutationPath` returns either an object/array or `unknown`, never a literal/union. - So once a binding has one recorded write, it cannot evaluate to a primitive, and the check is needed only for its first write. - Capture-point evaluations use separate caches and are unaffected. - **Single-pass fold** (`javascriptSemanticMutationValues.ts`, `javascriptSemanticValues.ts`). - `invalidateSemanticMutationPaths(value, paths)` groups paths per slot and recurses once per slot. - It reproduces the sequential order-sensitive rules exactly: - any `null`/empty key ⇒ the same root `unknown`, which subsumes the existing early break; - the last array `length` write resets items; - an unmatched exact key appends one `unknown-coverage` slot; - later paths into an added slot are inert; - selectors still test every slot. - It replaces the single-path function, which had no other callers. Not included: no caps or truncation, no change to cycle handling from `28595276`, and no contract, schema, or catalog change. ## Contract and boundary impact - Semantic owner and earliest changed stage: `src/domain/javascript` member-mutation collection and binding evaluation. - CLI and MCP/tool-catalog contract: none. - Provider, bridge, target-format, or platform compatibility: none. - Evidence, artifact, provenance, or reconstruction contract: none. Values are identical (see below). - Process execution, authorization, cleanup, or containment impact: none. - Generated metadata, package, or installation impact: none. ## Evidence and regression coverage - **Test added:** `javascriptSemanticAnalysis.member_mutations.test.ts` › "records each write to one object without replaying earlier writes". - It writes 2,000 properties to one object, then asserts that the untouched slot stays `literal` and the written slot is `unknown`, within 2 s. - On the base revision it fails after 43 s (`expected 42984 to be less than 2000`). - **Base reproduction** on Node 24.18.0 / macOS arm64: | Writes to one object | `main` `0b9815b6` | this change | |---|---|---| | 1,000 | 3.4 s | 0.07 s | | 2,000 | 27.4 s | 0.09 s | | 4,000 | 202.9 s | 0.16 s | | 8,000 | — | 0.29 s | - Notion Desktop 7.37.1 bundles: `.webpack/main/index.js` (4.2 MB) 10.6 s → 8.7 s; `.webpack/renderer/activity_monitor/index.js` 0.98 s → 0.95 s. - **Value equivalence:** - Across all 96 JavaScript files in Notion Desktop 7.37.1's `app.asar`, all 58,900 binding and return-site values are byte-identical between `main` and this change. - 200,000 randomized value and path sequences (selectors, `null` keys, `length`, duplicate names, nesting) fold identically to the sequential per-path `reduce`. - **Commands run:** - `npm run check:fast`, which passes. - `npm run test:local -- src/domain/javascript/*.test.ts src/application/javascript/*.test.ts`: 55 files, 989 tests, passing. - `npm run test:focused --` with the 42 JavaScript CLI/MCP/composition test files: 286/290 passed. All 4 failures were 30 s timeouts in `tests/acceptance/applications/exportedFunctionBindingsCli.test.ts` and `tests/acceptance/tools/localToolContracts.test.ts`. Run alone, those two files also time out on unmodified `main` on this machine (5 failures there vs 3 with this change). - **Real Hopper/Ghidra verification:** not applicable; JavaScript-only change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-17 21:11:28 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2891
No description provided.