[GH-ISSUE #6311] analyze_javascript_application hangs at parse_javascript_source on 664KB single-line ESM UMD (@castleio/castle-js) #3811

Open
opened 2026-10-31 00:44:18 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @jeremy9682 on GitHub (Oct 22, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6311

Originally assigned to: @kaoru0822-kitauji on GitHub.

Summary / observed behavior

analyze_javascript_application does not produce a result for a single 664 KB minified @castleio/castle-js vendor bundle. All three recorded CLI runs remained at parse_javascript_source, with the message Parsing and projecting castle.js (1/1), until the main process was externally terminated after approximately 75 seconds. No evidence was returned.

The progress event combines parsing and projection, so these observations do not establish whether the parser or semantic projection is responsible, or whether this is an infinite loop rather than unusually slow processing.

Environment

  • REA: rea-agents 6.7.0, using a local source build via /usr/local/bin/rea.
  • Checkout revision: bffcb2e3da7e48fc0078844502a39c54a4e37987. The compiled artifact was not verified to match this revision exactly.
  • OS: macOS / Darwin. Exact macOS version and architecture: NOT_ENOUGH_INFO in the saved evidence.
  • Node.js: v22.22.0.
  • Area: CLI. Interface: CLI.
  • Earliest failure stage: Unknown in the issue template's categories; the last reported JavaScript analysis phase is parse_javascript_source.
  • Last known good version / regression range: NOT_ENOUGH_INFO; no version comparison was performed.

Input / target and artifact context

  • One file: input/castle.js, identified by its header as @castleio/castle-js.
  • Vendor bundle described by the reporter as ESM/UMD; the prior inspection recorded a default export at the end.
  • Size: 664,306 bytes.
  • Physical lines: 6, independently counted from the unchanged input during report preparation. This is a densely minified bundle, not literally a one-physical-line file.
  • SHA256: a6c454743e5a9bee6211bffad2db559cb9066796435239eeb57b0d427ad09a66.
  • The saved precheck records node --check input/castle.js using Node v22.22.0 with exit code 0 and empty stdout/stderr. This checks syntax; the target code was not executed, and it does not establish compatibility with REA's parser/projector.
  • The input was not rewritten, split, beautified, or otherwise transformed. This is a single-file fixture, not a reduced source-level reproducer.
  • Cannot attach third-party code. No vendor source file or source excerpt is included. The suggested reproducible fixture is the corresponding distribution file from the public @castleio/castle-js npm package; exact package version and dist path still need to be matched to the SHA256 above (see Suggested fixture).

Command / reproduction

The saved experiment ran one three-variant matrix, with a directory containing only the original castle.js file. The base invocation was:

/usr/local/bin/rea analyze-javascript-application "$PWD/input" \
  --format json \
  --filter-output evidence_id,normalized_result.statistics \
  --max-heap-mb 4096 \
  --analysis-timeout-ms 600000

For the second variant, append:

--max-semantic-nodes 1 \
--max-semantic-nodes-per-file 1 \
--max-semantic-unknowns 1 \
--max-semantic-unknowns-per-file 1

For the third variant, use the base invocation with --max-heap-mb 1024, without the semantic-limit overrides.

The harness set a 75-second external timeout for each main process. The REA setting --analysis-timeout-ms 600000 is a separate internal timeout; this matrix did not wait for that timeout. HOME, TMPDIR, XDG_CACHE_HOME, and CLANG_MODULE_CACHE_PATH were redirected to directories inside the experiment's scratch workspace. No new analysis run was performed while preparing this issue.

Expected behavior / typed outcome or recovery expected

Expected by the reporter: complete analysis and return an evidence identifier/result, or return an explicit, bounded timeout/resource error that can be handled by the caller. Cancellation should leave no analysis worker running. This is an expectation, not a claim that a documented completion-time guarantee was violated.

Actual behavior / relevant output

All three saved stderr logs end with the same event:

{"rea_progress":{"phase":"parse_javascript_source","completed":0,"total":1,"message":"Parsing and projecting castle.js (1/1)","sequence":4}}

Each stdout is empty. The matrix records exit: null, killed: true, and has_evidence: false for every variant. No OOM or V8 heap-fatal diagnostic appears in the captured stderr.

Matrix

Variant Heap limit Semantic limits Main-process elapsed time Last phase Stdout / evidence
Default settings, 4 GB heap 4096 MB Default 75.01 s; externally killed parse_javascript_source 0 bytes / none
Semantic limits = 1 4096 MB All four limits set to 1 75.01 s; externally killed parse_javascript_source 0 bytes / none
1 GB heap 1024 MB Default 75.02 s; externally killed parse_javascript_source 0 bytes / none

Additional reporter observations, outside this saved matrix: approximately 100% CPU while stuck at this phase, and previous 300-second and 1,200-second attempts also failed to produce a result. CPU samples, commands, and logs for those longer attempts are not present in the supplied evidence; they have not been independently verified during preparation of this issue. They should not be treated as measurements from the three rows above.

Timing / cancellation limitation: the harness used p.kill() on the main process only. Three javascriptAnalysisWorkerProcess.js workers survived as orphan processes and were subsequently identified by their experiment-owned working directories and explicitly killed. At cleanup, their recorded running times were approximately 223 / 149 / 73 seconds. The first two workers therefore outlived the 75-second main-process windows and overlapped later variants. These are not strictly isolated 75-second process-tree experiments, and the elapsed times must not be used as comparative performance measurements. Final cleanup evidence records no remaining known experiment processes.

Impact and scope

This one vendor file prevents full-file REA analysis coverage of the application under inspection: the original file still has no REA evidence. Omitting it or analyzing a transformed derivative would leave that original-file coverage gap.

No working workaround was found among the tested semantic-limit and heap flags. This does not establish that every possible REA configuration fails. Frequency within the saved matrix: 3 of 3 runs reached the same last phase without a result before external termination. Behavior for other package versions and inputs is untested.

Workarounds tried

  • Set all four semantic node/unknown limits to 1: no visible phase change or result within the observation window.
  • Reduced the heap limit from 4096 MB to 1024 MB: no result; no OOM diagnostic was observed in the saved window. Longer-run memory behavior is unknown.
  • Longer 300 s / 1200 s waits: reported unsuccessful by the reporter, with the evidence limitation noted above.
  • External cancellation: stopped the parent but left workers that required explicit cleanup. The reproduction harness also needs process-tree cancellation.

No transformed-input workaround was tested, and none is claimed as equivalent coverage of the original file.

Suggested fixture and diagnostic improvements

Use the matching public npm package @castleio/castle-js distribution file as an upstream fixture, in a directory with no other inputs. Package page: https://www.npmjs.com/package/@castleio/castle-js . Match the 664,306-byte size and SHA256 above before claiming an exact reproduction.

Exact npm version and distribution filename: NOT_ENOUGH_INFO in the permitted local evidence. Retrieval from npm and byte-for-byte equivalence were not tested during issue preparation; the package is a suggested fixture source, not a newly verified reproduction.

Separate parser start/end and projection start/end progress/timing would help locate the expensive step. Please also clarify when semantic limits take effect. A bounded upstream CPU profile could distinguish slow parsing from slow projection; no CPU stack profile is included here. The observed worker lifecycle after external parent termination may also warrant cancellation handling, alongside fixing the harness to terminate the full process tree.

Checklist

  • Searched open and closed issues with gh issue list -R morluto/rea --state all --search 'castle parse_javascript_source' --limit 20; no matches were returned.
  • Read the local repository's bug-report template and supplied the relevant context, reproduction, expected/actual behavior, and evidence limitations.
  • No credentials, tokens, secrets, account identifiers, private user paths, or customer data included.
  • No third-party code file attached and no vendor source excerpt pasted.
  • Recorded observations, additional reporter observations, unknowns, and proposed diagnostics are distinguished.
Originally created by @jeremy9682 on GitHub (Oct 22, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6311 Originally assigned to: @kaoru0822-kitauji on GitHub. ## Summary / observed behavior `analyze_javascript_application` does not produce a result for a single 664 KB minified `@castleio/castle-js` vendor bundle. All three recorded CLI runs remained at `parse_javascript_source`, with the message `Parsing and projecting castle.js (1/1)`, until the main process was externally terminated after approximately 75 seconds. No evidence was returned. The progress event combines parsing and projection, so these observations do **not** establish whether the parser or semantic projection is responsible, or whether this is an infinite loop rather than unusually slow processing. ## Environment - REA: `rea-agents` **6.7.0**, using a local source build via `/usr/local/bin/rea`. - Checkout revision: `bffcb2e3da7e48fc0078844502a39c54a4e37987`. The compiled artifact was not verified to match this revision exactly. - OS: **macOS / Darwin**. Exact macOS version and architecture: `NOT_ENOUGH_INFO` in the saved evidence. - Node.js: **v22.22.0**. - Area: CLI. Interface: CLI. - Earliest failure stage: Unknown in the issue template's categories; the last reported JavaScript analysis phase is `parse_javascript_source`. - Last known good version / regression range: `NOT_ENOUGH_INFO`; no version comparison was performed. ## Input / target and artifact context - One file: `input/castle.js`, identified by its header as `@castleio/castle-js`. - Vendor bundle described by the reporter as ESM/UMD; the prior inspection recorded a default export at the end. - Size: **664,306 bytes**. - Physical lines: **6**, independently counted from the unchanged input during report preparation. This is a densely minified bundle, not literally a one-physical-line file. - SHA256: `a6c454743e5a9bee6211bffad2db559cb9066796435239eeb57b0d427ad09a66`. - The saved precheck records `node --check input/castle.js` using Node v22.22.0 with exit code **0** and empty stdout/stderr. This checks syntax; the target code was not executed, and it does not establish compatibility with REA's parser/projector. - The input was not rewritten, split, beautified, or otherwise transformed. This is a single-file fixture, not a reduced source-level reproducer. - **Cannot attach third-party code.** No vendor source file or source excerpt is included. The suggested reproducible fixture is the corresponding distribution file from the public `@castleio/castle-js` npm package; exact package version and dist path still need to be matched to the SHA256 above (see Suggested fixture). ## Command / reproduction The saved experiment ran one three-variant matrix, with a directory containing only the original `castle.js` file. The base invocation was: ```sh /usr/local/bin/rea analyze-javascript-application "$PWD/input" \ --format json \ --filter-output evidence_id,normalized_result.statistics \ --max-heap-mb 4096 \ --analysis-timeout-ms 600000 ``` For the second variant, append: ```sh --max-semantic-nodes 1 \ --max-semantic-nodes-per-file 1 \ --max-semantic-unknowns 1 \ --max-semantic-unknowns-per-file 1 ``` For the third variant, use the base invocation with `--max-heap-mb 1024`, without the semantic-limit overrides. The harness set a **75-second external timeout for each main process**. The REA setting `--analysis-timeout-ms 600000` is a separate internal timeout; this matrix did not wait for that timeout. `HOME`, `TMPDIR`, `XDG_CACHE_HOME`, and `CLANG_MODULE_CACHE_PATH` were redirected to directories inside the experiment's scratch workspace. No new analysis run was performed while preparing this issue. ## Expected behavior / typed outcome or recovery expected Expected by the reporter: complete analysis and return an evidence identifier/result, or return an explicit, bounded timeout/resource error that can be handled by the caller. Cancellation should leave no analysis worker running. This is an expectation, not a claim that a documented completion-time guarantee was violated. ## Actual behavior / relevant output All three saved stderr logs end with the same event: ```json {"rea_progress":{"phase":"parse_javascript_source","completed":0,"total":1,"message":"Parsing and projecting castle.js (1/1)","sequence":4}} ``` Each stdout is empty. The matrix records `exit: null`, `killed: true`, and `has_evidence: false` for every variant. No OOM or V8 heap-fatal diagnostic appears in the captured stderr. ## Matrix | Variant | Heap limit | Semantic limits | Main-process elapsed time | Last phase | Stdout / evidence | | --- | --- | --- | --- | --- | --- | | Default settings, 4 GB heap | 4096 MB | Default | 75.01 s; externally killed | `parse_javascript_source` | 0 bytes / none | | Semantic limits = 1 | 4096 MB | All four limits set to 1 | 75.01 s; externally killed | `parse_javascript_source` | 0 bytes / none | | 1 GB heap | 1024 MB | Default | 75.02 s; externally killed | `parse_javascript_source` | 0 bytes / none | **Additional reporter observations, outside this saved matrix:** approximately **100% CPU** while stuck at this phase, and previous **300-second and 1,200-second attempts also failed to produce a result**. CPU samples, commands, and logs for those longer attempts are not present in the supplied evidence; they have not been independently verified during preparation of this issue. They should not be treated as measurements from the three rows above. **Timing / cancellation limitation:** the harness used `p.kill()` on the main process only. Three `javascriptAnalysisWorkerProcess.js` workers survived as orphan processes and were subsequently identified by their experiment-owned working directories and explicitly killed. At cleanup, their recorded running times were approximately 223 / 149 / 73 seconds. The first two workers therefore outlived the 75-second main-process windows and overlapped later variants. These are **not strictly isolated 75-second process-tree experiments**, and the elapsed times must not be used as comparative performance measurements. Final cleanup evidence records no remaining known experiment processes. ## Impact and scope This one vendor file prevents full-file REA analysis coverage of the application under inspection: the original file still has no REA evidence. Omitting it or analyzing a transformed derivative would leave that original-file coverage gap. No working workaround was found among the tested semantic-limit and heap flags. This does not establish that every possible REA configuration fails. Frequency within the saved matrix: **3 of 3 runs** reached the same last phase without a result before external termination. Behavior for other package versions and inputs is untested. ## Workarounds tried - Set all four semantic node/unknown limits to **1**: no visible phase change or result within the observation window. - Reduced the heap limit from **4096 MB to 1024 MB**: no result; no OOM diagnostic was observed in the saved window. Longer-run memory behavior is unknown. - Longer **300 s / 1200 s** waits: reported unsuccessful by the reporter, with the evidence limitation noted above. - External cancellation: stopped the parent but left workers that required explicit cleanup. The reproduction harness also needs process-tree cancellation. No transformed-input workaround was tested, and none is claimed as equivalent coverage of the original file. ## Suggested fixture and diagnostic improvements Use the matching **public npm package `@castleio/castle-js` distribution file** as an upstream fixture, in a directory with no other inputs. Package page: https://www.npmjs.com/package/@castleio/castle-js . Match the 664,306-byte size and SHA256 above before claiming an exact reproduction. Exact npm version and distribution filename: **`NOT_ENOUGH_INFO`** in the permitted local evidence. Retrieval from npm and byte-for-byte equivalence were not tested during issue preparation; the package is a suggested fixture source, not a newly verified reproduction. Separate parser start/end and projection start/end progress/timing would help locate the expensive step. Please also clarify when semantic limits take effect. A bounded upstream CPU profile could distinguish slow parsing from slow projection; no CPU stack profile is included here. The observed worker lifecycle after external parent termination may also warrant cancellation handling, alongside fixing the harness to terminate the full process tree. ## Checklist - [x] Searched open and closed issues with `gh issue list -R morluto/rea --state all --search 'castle parse_javascript_source' --limit 20`; no matches were returned. - [x] Read the local repository's bug-report template and supplied the relevant context, reproduction, expected/actual behavior, and evidence limitations. - [x] No credentials, tokens, secrets, account identifiers, private user paths, or customer data included. - [x] No third-party code file attached and no vendor source excerpt pasted. - [x] Recorded observations, additional reporter observations, unknowns, and proposed diagnostics are distinguished.
Author
Owner

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

Thanks, jeremy9682, for the detailed matrix.

Your harness killed each run at 75 seconds. REA's own deadline was the 600000 ms you set with --analysis-timeout-ms. So the runs never reached it, and the logs can't tell "stuck" from "slow".

To tell them apart, repeat the base command with --analysis-timeout-ms 60000, under your 75 s kill. The setting's description says what happens on timeout. REA returns the partial Evidence it collected and stops. If it still doesn't stop, please say so here. That would be a real hang.

Not run: I did not run your file or this command.

🤖 Written with Claude Code (Sonnet 5.5, verified by Opus 5.5), posted with my OK 🦊

<!-- gh-comment-id:6370384639 --> @CassieFox23 commented on GitHub (Oct 23, 2026): Thanks, jeremy9682, for the detailed matrix. Your harness killed each run at 75 seconds. REA's own deadline was the 600000 ms you set with `--analysis-timeout-ms`. So the runs never reached it, and the logs can't tell "stuck" from "slow". To tell them apart, repeat the base command with `--analysis-timeout-ms 60000`, under your 75 s kill. The setting's description says what happens on timeout. REA returns the partial Evidence it collected and stops. If it still doesn't stop, please say so here. That would be a real hang. Not run: I did not run your file or this command. 🤖 Written with Claude Code (Sonnet 5.5, verified by Opus 5.5), posted with my OK 🦊
Author
Owner

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

Result: this is slow processing, not a hang. REA stops at its deadline and returns partial Evidence. The time goes into a step that grows cubically, in ambient-write indexing (indexJavaScriptAmbientWritesSteps), during the worker's static phase. The open PRs #6146, #2041 and #6477 don't fix it.

What ran (main bc143125e, macOS arm64, Node 26.5.0, built CLI)

Fixture: none of the npm dist/ files matches the reported SHA256 / 664,306 bytes. The closest one I found is jsDelivr's ESM build of @castleio/castle-js@2.8.2 (https://cdn.jsdelivr.net/npm/@castleio/castle-js@2.8.2/+esm): 664,054 bytes, 8 lines, export { EK as default } at the end, sha256 8c041920539125c1a3ea76fdde7c8187336f58a964b6f0475b495b4527192fd0. It is not byte-identical to the reported file.

/usr/bin/time -l node scripts/rea.mjs analyze-javascript-application <dir-with-castle.js> \
  --format json --analysis-timeout-ms 60000 --max-heap-mb 2048
  • Wall time 62.35 s, user 64.62 s, peak RSS 744 MB (/usr/bin/time -l, parent process; the worker is a separate process).
  • Exit 1 with provider_timeout and a details.partial_observation that has an evidence_id. No analysis worker was left running afterwards.
  • The last progress events were javascript_worker_parse and then javascript_worker_static. Current main reports worker phases, so the stall is located after parsing.

Where the time goes (in-process CPU profile, static phase, first 30 s)

The parse took about 0.2 s. Of the 30 s of samples, 88.5% are under indexJavaScriptAmbientWritesSteps, in the reference-store fixpoint (pendingStores / dependents) around createAmbientPathResolver(state, true, …). The top self frames are the resolver closure (40%), electronMemberPath (13%) and javaScriptExpressionWrapperTarget (12%). semanticEffectTiming / statementPosition (#4601) and member-mutation traversal (#6146) do not appear.

Counters I added to the build output (not committed):

  • The file has 6,916 reference stores. Most look like t[<computed>] = <function / object / call> on one large object t.
  • Resolver work per fixpoint query grows with the number of stores already published: about 1.1k pops on average for the first 300 queries, about 37k by query 2,100, and up to 237k pops (about 2.4k distinct roots) for a single query.
  • Already-processed sources keep being re-queued: after 1,800 queries, 6,748 were still pending.

Minimal synthetic reproducer (no third-party code)

// n stores of fresh objects into one local object at unknown (computed) keys
let s = "export default (function(){var t={};var k=[];";
for (let i = 0; i < n; i++) s += `t[k[${i}]]={v:${i}};`;
s += "return t})();";
shape (n = 125 → 250) static phase
t[k[i]] = {v:i} 0.58 s → 4.59 s
t[k[i]] = function(){…} 0.58 s → 4.58 s
t[k[i]] = [i] 0.55 s → 4.97 s
t.p_i = function(){…} (static key) 0.05 s → 0.07 s
t[k[i]] = i (primitive) 0.05 s → 0.09 s

Doubling n multiplies the time by about 8, which is cubic. At n = 500 the static phase alone did not finish within 40 s. Through the public CLI with --analysis-timeout-ms 30000:

  • n = 250 (4,338 bytes): finishes in 10.6 s, peak RSS 385 MB.
  • n = 500 (8,838 bytes): provider_timeout at 31.3 s.

indexSemanticAmbientWritesSteps also runs in the semantic phase and in analyze_web_bundle (webBundleAnalyzerInspection.ts), so the same cost can show up in both places.

Cause, read from the source (not verified at runtime)

The cause is in the fixpoint in indexJavaScriptAmbientWritesSteps:

  • For each reference store, the fixpoint resolves the identities of the target (source.target) without caching (possibleWrites = true).
  • When a fresh object has been written at unknown keys, a walk that reaches it follows every value already stored there with an empty remaining path. It then records each of those occupants as an identity of the new store, at path [].
  • As a result, every stored value ends up holding an entry for every source, so the stores facts grow as O(n²). Each walk is O(n) to O(n²), and each new store into the shared object re-queues every dependent source. That gives about O(n³) in total.
  • These path-[] "slot occupant" identities also look broader than the runtime semantics: overwriting t[k] does not mutate the object that previously occupied the slot.

Lowering the semantic node/unknown limits doesn't help, because this index runs before those limits apply. That matches the "semantic limits = 1" row in the report.

Why there is no PR yet

A fix needs a decision on what this owner should return. Options:

  • (a) Resolve the receiver of a store and drop slot-occupant identities. This changes ambient-write facts and must be checked against rewriting steps such as __proto__ → prototype and copied initializers.
  • (b) Collapse mutually aliased fresh values into one shared class.
  • (c) Add an explicit work bound that conservatively widens ambient writes and escapes to ["global", null], recorded as a reported resource limit.

#6477 also restructures this file. I'd rather settle on the approach before writing a change that conflicts with it.

Not run: the reporter's exact file (no matching hash found), Node 22, or the analysis without a deadline. Completion time without a deadline is unknown. The fixpoint works over a finite lattice and kept making progress in every sample, so my conclusion that it eventually finishes is based on reading the source, not on a measured run.

🤖 Generated with Claude Code

<!-- gh-comment-id:6396246657 --> @kaoru0822-kitauji commented on GitHub (Oct 25, 2026): **Result:** this is slow processing, not a hang. REA stops at its deadline and returns partial Evidence. The time goes into a step that grows cubically, in ambient-write indexing (`indexJavaScriptAmbientWritesSteps`), during the worker's `static` phase. The open PRs #6146, #2041 and #6477 don't fix it. ### What ran (main `bc143125e`, macOS arm64, Node 26.5.0, built CLI) Fixture: none of the npm `dist/` files matches the reported SHA256 / 664,306 bytes. The closest one I found is jsDelivr's ESM build of `@castleio/castle-js@2.8.2` (`https://cdn.jsdelivr.net/npm/@castleio/castle-js@2.8.2/+esm`): 664,054 bytes, 8 lines, `export { EK as default }` at the end, sha256 `8c041920539125c1a3ea76fdde7c8187336f58a964b6f0475b495b4527192fd0`. It is not byte-identical to the reported file. ```sh /usr/bin/time -l node scripts/rea.mjs analyze-javascript-application <dir-with-castle.js> \ --format json --analysis-timeout-ms 60000 --max-heap-mb 2048 ``` - Wall time 62.35 s, user 64.62 s, peak RSS 744 MB (`/usr/bin/time -l`, parent process; the worker is a separate process). - Exit 1 with `provider_timeout` and a `details.partial_observation` that has an `evidence_id`. No analysis worker was left running afterwards. - The last progress events were `javascript_worker_parse` and then `javascript_worker_static`. Current main reports worker phases, so the stall is located after parsing. ### Where the time goes (in-process CPU profile, static phase, first 30 s) The parse took about 0.2 s. Of the 30 s of samples, 88.5% are under `indexJavaScriptAmbientWritesSteps`, in the reference-store fixpoint (`pendingStores` / `dependents`) around `createAmbientPathResolver(state, true, …)`. The top self frames are the resolver closure (40%), `electronMemberPath` (13%) and `javaScriptExpressionWrapperTarget` (12%). `semanticEffectTiming` / `statementPosition` (#4601) and member-mutation traversal (#6146) do not appear. Counters I added to the build output (not committed): - The file has 6,916 reference stores. Most look like `t[<computed>] = <function / object / call>` on one large object `t`. - Resolver work per fixpoint query grows with the number of stores already published: about 1.1k pops on average for the first 300 queries, about 37k by query 2,100, and up to 237k pops (about 2.4k distinct roots) for a single query. - Already-processed sources keep being re-queued: after 1,800 queries, 6,748 were still pending. ### Minimal synthetic reproducer (no third-party code) ```js // n stores of fresh objects into one local object at unknown (computed) keys let s = "export default (function(){var t={};var k=[];"; for (let i = 0; i < n; i++) s += `t[k[${i}]]={v:${i}};`; s += "return t})();"; ``` | shape (n = 125 → 250) | static phase | | --- | --- | | `t[k[i]] = {v:i}` | 0.58 s → 4.59 s | | `t[k[i]] = function(){…}` | 0.58 s → 4.58 s | | `t[k[i]] = [i]` | 0.55 s → 4.97 s | | `t.p_i = function(){…}` (static key) | 0.05 s → 0.07 s | | `t[k[i]] = i` (primitive) | 0.05 s → 0.09 s | Doubling n multiplies the time by about 8, which is cubic. At n = 500 the static phase alone did not finish within 40 s. Through the public CLI with `--analysis-timeout-ms 30000`: - n = 250 (4,338 bytes): finishes in 10.6 s, peak RSS 385 MB. - n = 500 (8,838 bytes): `provider_timeout` at 31.3 s. `indexSemanticAmbientWritesSteps` also runs in the semantic phase and in `analyze_web_bundle` (`webBundleAnalyzerInspection.ts`), so the same cost can show up in both places. ### Cause, read from the source (not verified at runtime) The cause is in the fixpoint in `indexJavaScriptAmbientWritesSteps`: - For each reference store, the fixpoint resolves the identities of the target (`source.target`) without caching (`possibleWrites = true`). - When a fresh object has been written at unknown keys, a walk that reaches it follows every value already stored there with an empty remaining path. It then records each of those occupants as an identity of the new store, at path `[]`. - As a result, every stored value ends up holding an entry for every source, so the `stores` facts grow as O(n²). Each walk is O(n) to O(n²), and each new store into the shared object re-queues every dependent source. That gives about O(n³) in total. - These path-`[]` "slot occupant" identities also look broader than the runtime semantics: overwriting `t[k]` does not mutate the object that previously occupied the slot. Lowering the semantic node/unknown limits doesn't help, because this index runs before those limits apply. That matches the "semantic limits = 1" row in the report. ### Why there is no PR yet A fix needs a decision on what this owner should return. Options: - (a) Resolve the receiver of a store and drop slot-occupant identities. This changes ambient-write facts and must be checked against rewriting steps such as `__proto__` → `prototype` and copied initializers. - (b) Collapse mutually aliased fresh values into one shared class. - (c) Add an explicit work bound that conservatively widens ambient writes and escapes to `["global", null]`, recorded as a reported resource limit. #6477 also restructures this file. I'd rather settle on the approach before writing a change that conflicts with it. Not run: the reporter's exact file (no matching hash found), Node 22, or the analysis without a deadline. Completion time without a deadline is unknown. The fixpoint works over a finite lattice and kept making progress in every sample, so my conclusion that it eventually finishes is based on reading the source, not on a measured run. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

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

Update: #7128 fixes the cubic slot-occupant cost described above. A synthetic file of n stores t[k[i]] = {…} now finishes in about 2 s through the CLI. Before the change, n=500 took 97 s and n=1000 took 618 s. The CastleIO 2.8.2 ESM build still hits the deadline, so #7128 uses Refs, not Fixes.

What remains (profiled on the #7128 build, Node 26.5.0, macOS arm64):

  • With a 900 s deadline, castle times out in the worker static phase: 901 s wall, 970 s user CPU, 1.65 GB peak RSS for a single process.
  • The time is still in the reference-store fixpoint of indexJavaScriptAmbientWritesSteps. By fixpoint iteration 4,000, 148.25M of the 148.5M resolver worklist pops were for any-depth (undefined) projections.
  • The widening starts at local register arrays ([]) that store arrays inside each other. A projection first reaches them at [null,null] and then at [null,null,null], so it widens to any depth. This widening is legitimate, not the slot-occupant aliasing #7128 removes.
  • After that, every store that reaches this region walks the region's whole closure again, about 190k pops per walk late in the fixpoint. Each walk also publishes an any-depth entry onto every fresh object in the region.

Likely fix direction: a shared summary for widened regions, so that each store does not get its own entries and closure walk. That is a representation change in the same owner, so I'm leaving it out of #7128.

🤖 Generated with Claude Code

<!-- gh-comment-id:6396929547 --> @kaoru0822-kitauji commented on GitHub (Oct 25, 2026): Update: #7128 fixes the cubic slot-occupant cost described above. A synthetic file of n stores `t[k[i]] = {…}` now finishes in about 2 s through the CLI. Before the change, n=500 took 97 s and n=1000 took 618 s. The CastleIO 2.8.2 ESM build still hits the deadline, so #7128 uses `Refs`, not `Fixes`. **What remains** (profiled on the #7128 build, Node 26.5.0, macOS arm64): - With a 900 s deadline, castle times out in the worker `static` phase: 901 s wall, 970 s user CPU, 1.65 GB peak RSS for a single process. - The time is still in the reference-store fixpoint of `indexJavaScriptAmbientWritesSteps`. By fixpoint iteration 4,000, 148.25M of the 148.5M resolver worklist pops were for any-depth (`undefined`) projections. - The widening starts at local register arrays (`[]`) that store arrays inside each other. A projection first reaches them at `[null,null]` and then at `[null,null,null]`, so it widens to any depth. This widening is legitimate, not the slot-occupant aliasing #7128 removes. - After that, every store that reaches this region walks the region's whole closure again, about 190k pops per walk late in the fixpoint. Each walk also publishes an any-depth entry onto every fresh object in the region. **Likely fix direction:** a shared summary for widened regions, so that each store does not get its own entries and closure walk. That is a representation change in the same owner, so I'm leaving it out of #7128. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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#3811
No description provided.