[GH-ISSUE #1495] [Bug] Many methods returning one object make analysis grow quadratically since #1471 #510

Closed
opened 2026-10-15 18:25:42 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @CassieFox23 on GitHub (Oct 10, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1495

TL;DR: Since #1471, many methods that return one object make analysis slow. Time grows about with the square of the call count. At 2,000 methods it takes 7.6 s, against 0.8 s before.

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

The shape is one object with N methods that each return the object, then N calls that write through them:

export function result() {
  const shared = { token: "TOKEN" };
  const box = { m0() { return shared; }, m1() { return shared; } /* … up to N */ };
  box.m0().p0 = 0;
  box.m1().p1 = 1;
  // … up to N
  return [shared.token];
}

Analysis time on Node 24.18.0, median of 3. Before is 4e3d3354, and after is #1471's head 8414ee49 on top of it.

N Before After
250 94 ms 201 ms
500 159 ms 571 ms
1,000 341 ms 1,868 ms
2,000 826 ms 7,048 ms
4,000 2,601 ms 30,366 ms

Each doubling costs about 4 times as much at the top. The box was busy for that table. So I reran N = 2,000 on a quiet box, at a load average of 1.8. Load average counts programs running or waiting for one of the 24 CPU threads. Then 4e3d3354 took 794 ms and main 1f692c83e took 7,618 ms.

Reproduction

  1. Generate the file above for N = 2,000.
  2. Run rea analyze-javascript-application <folder> --artifact-format directory --json on main, then on 4e3d3354.
  3. Compare the analysis times.

A small generator script is in the fold below.

Expected behavior

About the same growth as before #1471. Before, the 2,000 to 4,000 step cost about 3 times as much.

Impact and scope

Bundles with large objects of methods that return this or a shared store hit this. Fluent builders are one example. On these four other shapes, after was within 16% of before:

  • 1,000 plain get().p1 = 1 calls
  • 1,000 captured calls like t1 = get()
  • a chain of 200 wrappers
  • the Hermes Desktop (MIT) vendor chunk

The new pass lives in javascriptSemanticReturnedReferences.ts. I did not profile it, so I don't know which step grows.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Not run

  • A CPU profile of the slow case.
  • MCP, and Node 22 and 26, on the slow case.
  • Real bundles with fluent builders.
  • Windows and macOS.

Found while testing #1471 for #1449.

Generator
// node gen.mjs 2000 > app.js
const n = Number(process.argv[2]);
const methods = Array.from({ length: n }, (_, i) => `m${i}() { return shared; },`).join(" ");
const calls = Array.from({ length: n }, (_, i) => `box.m${i}().p${i} = ${i};`).join(" ");
console.log(`export function result() { const shared = { token: "TOKEN" }; const box = { ${methods} }; ${calls} return [shared.token]; }`);
Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 24.18.0
  • REA: built from source with npm ci and npm run build; trees 4e3d3354, 8414ee49 and 1f692c83e

🤖 Generated with Claude Code (Opus 5.5) 🦊

Originally created by @CassieFox23 on GitHub (Oct 10, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1495 **TL;DR:** Since #1471, many methods that return one object make analysis slow. Time grows about with the square of the call count. At 2,000 methods it takes 7.6 s, against 0.8 s before. ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior The shape is one object with N methods that each return the object, then N calls that write through them: ```js export function result() { const shared = { token: "TOKEN" }; const box = { m0() { return shared; }, m1() { return shared; } /* … up to N */ }; box.m0().p0 = 0; box.m1().p1 = 1; // … up to N return [shared.token]; } ``` Analysis time on Node 24.18.0, median of 3. Before is `4e3d3354`, and after is #1471's head `8414ee49` on top of it. | N | Before | After | |---|---|---| | 250 | 94 ms | 201 ms | | 500 | 159 ms | 571 ms | | 1,000 | 341 ms | 1,868 ms | | 2,000 | 826 ms | 7,048 ms | | 4,000 | 2,601 ms | 30,366 ms | Each doubling costs about 4 times as much at the top. The box was busy for that table. So I reran N = 2,000 on a quiet box, at a load average of 1.8. Load average counts programs running or waiting for one of the 24 CPU threads. Then `4e3d3354` took 794 ms and main `1f692c83e` took 7,618 ms. ### Reproduction 1. Generate the file above for N = 2,000. 2. Run `rea analyze-javascript-application <folder> --artifact-format directory --json` on main, then on `4e3d3354`. 3. Compare the analysis times. A small generator script is in the fold below. ### Expected behavior About the same growth as before #1471. Before, the 2,000 to 4,000 step cost about 3 times as much. ### Impact and scope Bundles with large objects of methods that return `this` or a shared store hit this. Fluent builders are one example. On these four other shapes, after was within 16% of before: - 1,000 plain `get().p1 = 1` calls - 1,000 captured calls like `t1 = get()` - a chain of 200 wrappers - the [Hermes Desktop](https://github.com/NousResearch/hermes-agent) ([MIT](https://github.com/NousResearch/hermes-agent/blob/main/LICENSE)) vendor chunk The new pass lives in `javascriptSemanticReturnedReferences.ts`. I did not profile it, so I don't know which step grows. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Not run - A CPU profile of the slow case. - MCP, and Node 22 and 26, on the slow case. - Real bundles with fluent builders. - Windows and macOS. Found while testing #1471 for #1449. <details> <summary>Generator</summary> ```js // node gen.mjs 2000 > app.js const n = Number(process.argv[2]); const methods = Array.from({ length: n }, (_, i) => `m${i}() { return shared; },`).join(" "); const calls = Array.from({ length: n }, (_, i) => `box.m${i}().p${i} = ${i};`).join(" "); console.log(`export function result() { const shared = { token: "TOKEN" }; const box = { ${methods} }; ${calls} return [shared.token]; }`); ``` </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.9 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 24.18.0 - REA: built from source with `npm ci` and `npm run build`; trees `4e3d3354`, `8414ee49` and `1f692c83e` </details> 🤖 Generated with Claude Code (Opus 5.5) 🦊
Author
Owner

@kaoru0822-kitauji commented on GitHub (Oct 10, 2026):

Fixed in #1517. Each call through box escaped it as the receiver, and since #1471 every escape re-expanded all of box's methods to reach their results: N calls × N methods. Escapes now reuse that syntax-only expansion per expression and path, and they collapse identifiers that resolve to the same binding. Every escape site is still recorded. In-process semantic analysis on your shape: 2,000 → 253 ms (was 2,992 ms) and 4,000 → 638 ms (was 12,016 ms). The semantic IR is byte-identical to main on 50 sources, including the #1471 returned-reference cases and real VS Code/Obsidian files.

<!-- gh-comment-id:6095893685 --> @kaoru0822-kitauji commented on GitHub (Oct 10, 2026): Fixed in #1517. Each call through `box` escaped it as the receiver, and since #1471 every escape re-expanded all of `box`'s methods to reach their results: N calls × N methods. Escapes now reuse that syntax-only expansion per expression and path, and they collapse identifiers that resolve to the same binding. Every escape site is still recorded. In-process semantic analysis on your shape: 2,000 → 253 ms (was 2,992 ms) and 4,000 → 638 ms (was 12,016 ms). The semantic IR is byte-identical to main on 50 sources, including the #1471 returned-reference cases and real VS Code/Obsidian files.
Author
Owner

@CassieFox23 commented on GitHub (Oct 10, 2026):

TL;DR: Thanks, kaoru0822-kitauji, for the fast fix in #1517. The write-up of the N × N cause is clear too. That PR is still open, so main keeps the slowdown until it merges.

After it merges, I can rerun the 2,000-method timing on main.

Edit: #1517 merged, and I reran it, in the #1517 check. At 2,000 methods it went from 6.8 s to 0.5 s, and no answer changed.

🤖 Generated with Claude Code (Opus 5.5) 🦊

<!-- gh-comment-id:6095903594 --> @CassieFox23 commented on GitHub (Oct 10, 2026): **TL;DR:** Thanks, kaoru0822-kitauji, for the fast fix in #1517. The write-up of the N × N cause is clear too. That PR is still open, so main keeps the slowdown until it merges. After it merges, I can rerun the 2,000-method timing on main. Edit: #1517 merged, and I reran it, in [the #1517 check](https://github.com/morluto/rea/pull/1517#issuecomment-6096095887). At 2,000 methods it went from 6.8 s to 0.5 s, and no answer changed. 🤖 Generated with Claude Code (Opus 5.5) 🦊
Author
Owner

@N0zoM1z0 commented on GitHub (Oct 10, 2026):

Confirmed locally: the 2,000-method source reproduces on the frozen a0e5e9db build; the JavaScript application/domain computational sources are identical through current main 93ca2769. An in-process CPU profile measures 13,337.9 ms for complete semantic analysis. Most sampled work comes from markReceiver repeatedly expanding every method's returned references, rather than Babel parsing.

I have a source change that caches pure escape-syntax expansion per expression/path and coalesces equivalent binding endpoints. Binding lifetimes and recorded mutation effects stay in the existing collector; different binding IDs, paths, arrays/prototype effects and iterable fallback endpoints remain distinct. The same narrow profiled case measures 939.7 ms, with the complete 6,244,884-byte IR retaining exactly the same SHA-256 (e788a8bcf05a302f566bf383d473e3f861e6d8972bf8302a5131efedaa924d39). These are single diagnostic observations, not a response percentile or a full-app speed claim.

The rebuilt branch passes 259 focused tests across four files, including real compiled CLI/stdio-MCP return-shape and iteration workflows, adjacent binding/path cases, member mutations and reference lifetimes. Strict typecheck/lint also pass. I am completing the paired large-case CLI/MCP and growth checks before opening the PR. This addresses #1495 without claiming that the separate synchronous-control pauses in #1462 are all resolved.

<!-- gh-comment-id:6095971581 --> @N0zoM1z0 commented on GitHub (Oct 10, 2026): Confirmed locally: the 2,000-method source reproduces on the frozen `a0e5e9db` build; the JavaScript application/domain computational sources are identical through current main `93ca2769`. An in-process CPU profile measures **13,337.9 ms** for complete semantic analysis. Most sampled work comes from `markReceiver` repeatedly expanding every method's returned references, rather than Babel parsing. I have a source change that caches pure escape-syntax expansion per expression/path and coalesces equivalent binding endpoints. Binding lifetimes and recorded mutation effects stay in the existing collector; different binding IDs, paths, arrays/prototype effects and iterable fallback endpoints remain distinct. The same narrow profiled case measures **939.7 ms**, with the complete 6,244,884-byte IR retaining exactly the same SHA-256 (`e788a8bcf05a302f566bf383d473e3f861e6d8972bf8302a5131efedaa924d39`). These are single diagnostic observations, not a response percentile or a full-app speed claim. The rebuilt branch passes 259 focused tests across four files, including real compiled CLI/stdio-MCP return-shape and iteration workflows, adjacent binding/path cases, member mutations and reference lifetimes. Strict typecheck/lint also pass. I am completing the paired large-case CLI/MCP and growth checks before opening the PR. This addresses #1495 without claiming that the separate synchronous-control pauses in #1462 are all resolved.
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#510
No description provided.