[GH-ISSUE #8294] analyze_javascript_application exhausts its 1 GiB worker heap on react-dom 18.3.1 production build (132 KB) while three.module.js (1.3 MB) completes in 14 s #6531

Open
opened 2026-11-01 21:11:58 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @ambrius on GitHub (Oct 31, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8294

Before submitting

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

Area

JavaScript application analysis (analyze_javascript_application, MCP and CLI)

Observed behavior

analyze_javascript_application with default settings exhausts its isolated worker heap on the public react-dom 18.3.1 production UMD build (131,835 bytes) and skips the file, while a 10× larger public file completes quickly.

Target (public npm package, single file) Size Result (defaults)
react-dom@18.3.1 umd/react-dom.production.min.js (sha256 35f4f974f4b2bcd44da73963347f8952e341f83909e4498227d4e26b98f66f0d) 131,835 B 182.5 s, parsed_javascript_files: 0, skipped_javascript_files: 1, visited_ast_nodes: 0
three@0.170.0 build/three.module.js (sha256 ce1fa418de16a19495a9f72495580e3015d7745c296d3ce0485897f902ddedfb) 1,314,681 B 14.1 s, parsed_javascript_files: 1, visited_ast_nodes: 182883, findings: 531

Limitation returned for react-dom (verbatim):

react-dom.production.min.js: Resource constraint during analyze_javascript_application: Owned JavaScript worker exhausted its 1024 MiB V8 old-space heap (max_heap_mb) while analyzing react-dom.production.min.js during static

On a ~0.7–1.2 MB application bundle that embeds React 18 (minified or not), raising max_heap_mb to 3072 avoids the heap error but the analysis then exceeds analysis_timeout_ms (300000, then 900000 and 1200000 ms), returning provider_timeout. So the problem does not look like file size; it seems specific to react-dom's production code.

Reproduction

  1. npm pack react-dom@18.3.1 and extract package/umd/react-dom.production.min.js into an otherwise empty directory app/.
  2. Run rea analyze-javascript-application <abs path>/app --detail summary (or the MCP tool with {"input_path": "<abs path>/app", "detail": "summary"}).
  3. Observe the skipped file and the heap limitation above (react-dom run once via MCP; three.js control run once and succeeded).

Expected behavior

A 132 KB minified file should parse within the default heap/time budget, as larger files do (three.js above). If some construct makes static analysis pathological, a bounded partial result (syntax-level inventory) would be preferable to skipping the whole file. The exact cause is unknown to me.

Impact and scope

Any web application bundle that includes React 18's production build appears affected: the main application file is skipped, so the application graph is empty. Not tested: React 19, other bundlers, other hosts.

REA version

rea --version → 6.8.0 (npm rea-agents@6.8.0, installed with --omit=optional)

Runtime and platform

Node.js 24.22.2 (node:24-bookworm-slim container), Linux x86_64 (WSL2 kernel 6.18), 12 CPUs, container memory limit 5 GB, no network.

Target and artifact context

Directory input containing one minified UMD JavaScript file (public npm package), no source map.

Interface

MCP (stdio) for both repro files. The CLI rea analyze overview showed the same heap skip on the larger React 18 application bundle; I did not run the CLI on the react-dom file alone.

Originally created by @ambrius on GitHub (Oct 31, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8294 ### 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 JavaScript application analysis (`analyze_javascript_application`, MCP and CLI) ### Observed behavior `analyze_javascript_application` with default settings exhausts its isolated worker heap on the **public react-dom 18.3.1 production UMD build (131,835 bytes)** and skips the file, while a 10× larger public file completes quickly. | Target (public npm package, single file) | Size | Result (defaults) | |---|---|---| | `react-dom@18.3.1` `umd/react-dom.production.min.js` (sha256 `35f4f974f4b2bcd44da73963347f8952e341f83909e4498227d4e26b98f66f0d`) | 131,835 B | **182.5 s**, `parsed_javascript_files: 0`, `skipped_javascript_files: 1`, `visited_ast_nodes: 0` | | `three@0.170.0` `build/three.module.js` (sha256 `ce1fa418de16a19495a9f72495580e3015d7745c296d3ce0485897f902ddedfb`) | 1,314,681 B | **14.1 s**, `parsed_javascript_files: 1`, `visited_ast_nodes: 182883`, `findings: 531` | Limitation returned for react-dom (verbatim): > react-dom.production.min.js: Resource constraint during analyze_javascript_application: Owned JavaScript worker exhausted its 1024 MiB V8 old-space heap (max_heap_mb) while analyzing react-dom.production.min.js during static On a ~0.7–1.2 MB application bundle that embeds React 18 (minified or not), raising `max_heap_mb` to 3072 avoids the heap error but the analysis then exceeds `analysis_timeout_ms` (300000, then 900000 and 1200000 ms), returning `provider_timeout`. So the problem does not look like file size; it seems specific to react-dom's production code. ### Reproduction 1. `npm pack react-dom@18.3.1` and extract `package/umd/react-dom.production.min.js` into an otherwise empty directory `app/`. 2. Run `rea analyze-javascript-application <abs path>/app --detail summary` (or the MCP tool with `{"input_path": "<abs path>/app", "detail": "summary"}`). 3. Observe the skipped file and the heap limitation above (react-dom run once via MCP; three.js control run once and succeeded). ### Expected behavior A 132 KB minified file should parse within the default heap/time budget, as larger files do (three.js above). If some construct makes static analysis pathological, a bounded partial result (syntax-level inventory) would be preferable to skipping the whole file. The exact cause is unknown to me. ### Impact and scope Any web application bundle that includes React 18's production build appears affected: the main application file is skipped, so the application graph is empty. Not tested: React 19, other bundlers, other hosts. ### REA version `rea --version` → 6.8.0 (npm `rea-agents@6.8.0`, installed with `--omit=optional`) ### Runtime and platform Node.js 24.22.2 (`node:24-bookworm-slim` container), Linux x86_64 (WSL2 kernel 6.18), 12 CPUs, container memory limit 5 GB, no network. ### Target and artifact context Directory input containing one minified UMD JavaScript file (public npm package), no source map. ### Interface MCP (stdio) for both repro files. The CLI `rea analyze` overview showed the same heap skip on the larger React 18 application bundle; I did not run the CLI on the react-dom file alone.
Author
Owner

@arjunprabhulal commented on GitHub (Oct 31, 2026):

Reproduced on macOS arm64 with Node 24.22.1 and main at 6e9ee97a6, using the same file (sha256 35f4f974…6f0d). With defaults, the worker exhausted its 1024 MiB heap during static after 33 s, with parsed_javascript_files: 0.

Smaller trigger. React's minified update-queue drain, function wd(a,b,c,d){…} in that file (1,367 bytes), exhausts a 1024 MiB heap on its own after about 21 s of static analysis. It may also be a useful public trigger for #6127, whose original artifact is unavailable.

Where the growth happens (observed with instrumentation). It happens in a single markValue call in collectSemanticMemberMutationsSteps (one .next = … write). The traversal reaches SEMANTIC_ALIAS_SELECTION_LIMIT and records alias-selections, but the widened joins are keyed by path depth and capture vector. Reassigned list cursors (h=h.next, l=l.next={…}, r=h,h=r.next) add a different segment on each alias lap, so the join keys keep growing. The pending worklist passed 518,000 entries before the heap ran out.

Draft PR #8392 bounds the widened joins with the same budget and falls back to escaping the receiver. It stops the out-of-memory failure and the file is parsed. The full file then still hits the 300 s default timeout after the static stage, so it is a partial fix. Profiling points at returned-reference expansion, Electron possiblePath and hasCommonJsModuleSource as the remaining slow paths; I have not root-caused those yet.

<!-- gh-comment-id:6550539030 --> @arjunprabhulal commented on GitHub (Oct 31, 2026): Reproduced on macOS arm64 with Node 24.22.1 and `main` at `6e9ee97a6`, using the same file (sha256 `35f4f974…6f0d`). With defaults, the worker exhausted its 1024 MiB heap during `static` after 33 s, with `parsed_javascript_files: 0`. **Smaller trigger.** React's minified update-queue drain, `function wd(a,b,c,d){…}` in that file (1,367 bytes), exhausts a 1024 MiB heap on its own after about 21 s of static analysis. It may also be a useful public trigger for #6127, whose original artifact is unavailable. **Where the growth happens (observed with instrumentation).** It happens in a single `markValue` call in `collectSemanticMemberMutationsSteps` (one `.next = …` write). The traversal reaches `SEMANTIC_ALIAS_SELECTION_LIMIT` and records `alias-selections`, but the widened joins are keyed by path depth and capture vector. Reassigned list cursors (`h=h.next`, `l=l.next={…}`, `r=h,h=r.next`) add a different segment on each alias lap, so the join keys keep growing. The pending worklist passed 518,000 entries before the heap ran out. Draft PR #8392 bounds the widened joins with the same budget and falls back to escaping the receiver. It stops the out-of-memory failure and the file is parsed. The full file then still hits the 300 s default timeout after the static stage, so it is a partial fix. Profiling points at returned-reference expansion, Electron `possiblePath` and `hasCommonJsModuleSource` as the remaining slow paths; I have not root-caused those yet.
Author
Owner

@ambrius commented on GitHub (Nov 1, 2026):

Thank you for reproducing this and for the precise root-cause analysis.

Your minimal trigger (function wd(a,b,c,d){…}, React's update-queue drain) fits what we measured: the same file exhausts the default 1024 MiB heap, while three@0.170.0 build/three.module.js (10× larger) completes in 14 s with defaults. Raising max_heap_mb to 3072 on a larger React 18 application bundle only moved the failure to the analysis_timeout_ms budget (300 s, then 900 s and 1200 s). That matches your note that the remaining cost comes after the static stage.

We will retest the react-dom file and our application bundle on the next published rea-agents release and report the numbers here.

<!-- gh-comment-id:6554572707 --> @ambrius commented on GitHub (Nov 1, 2026): Thank you for reproducing this and for the precise root-cause analysis. Your minimal trigger (`function wd(a,b,c,d){…}`, React's update-queue drain) fits what we measured: the same file exhausts the default 1024 MiB heap, while `three@0.170.0` `build/three.module.js` (10× larger) completes in 14 s with defaults. Raising `max_heap_mb` to 3072 on a larger React 18 application bundle only moved the failure to the `analysis_timeout_ms` budget (300 s, then 900 s and 1200 s). That matches your note that the remaining cost comes after the static stage. We will retest the react-dom file and our application bundle on the next published `rea-agents` release and report the numbers here.
Author
Owner

@jgt87 commented on GitHub (Nov 3, 2026):

Another small public trigger, with a different shape: Ace 1.8.1 ace/commands/command_manager (1,796 bytes).

Same symptom on rea-agents 6.8.0 (latest on npm), Windows, through MCP: the worker exhausts its 1024 MiB old-space heap during static, with parsed_javascript_files: 0, skipped_javascript_files: 1, visited_ast_nodes: 0, parse_failures: 0.

Input. The ace.define("ace/commands/command_manager", …) call taken from ace-builds@1.8.1 (npm, BSD-3-Clause, integrity sha512-wjEQ4khMQYg9FfdEDoOtqdoHwcwFL48H0VB3te5b5A3eqHwxsTw8IX6+xzfisgborIb8dYU+1y9tcmtGFrCPIg==). It runs from ace.define("ace/commands/command_manager" up to the next ace.define(, with trailing ,/;/whitespace trimmed and ;\n appended. It is parsed only, never executed.

Variant Source file Bytes SHA-256 Result
minified src-min-noconflict/ace.js 1,796 eb5ad99736a082433ab0664bddbaacb94690a2681780f06e7c8169719a9ff6d1 1024 MiB heap exhausted in static
readable src-noconflict/ace.js 3,233 c2c4fcc08a1d2680efeee0d85da077443fca37b4592f0f8b986ff91b4815beb0 1024 MiB heap exhausted in static

So minification is not the trigger.

Minified input (1,796 bytes)
ace.define("ace/commands/command_manager",["require","exports","module","ace/lib/oop","ace/keyboard/hash_handler","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../keyboard/hash_handler").MultiHashHandler,s=e("../lib/event_emitter").EventEmitter,o=function(e,t){i.call(this,t,e),this.byName=this.commands,this.setDefaultHandler("exec",function(e){return e.args?e.command.exec(e.editor,e.args,e.event,!1):e.command.exec(e.editor,{},e.event,!0)})};r.inherits(o,i),function(){r.implement(this,s),this.exec=function(e,t,n){if(Array.isArray(e)){for(var r=e.length;r--;)if(this.exec(e[r],t,n))return!0;return!1}typeof e=="string"&&(e=this.commands[e]);if(!e)return!1;if(t&&t.$readOnly&&!e.readOnly)return!1;if(this.$checkCommandState!=0&&e.isAvailable&&!e.isAvailable(t))return!1;var i={editor:t,command:e,args:n};return i.returnValue=this._emit("exec",i),this._signal("afterExec",i),i.returnValue===!1?!1:!0},this.toggleRecording=function(e){if(this.$inReplay)return;return e&&e._emit("changeStatus"),this.recording?(this.macro.pop(),this.off("exec",this.$addCommandToMacro),this.macro.length||(this.macro=this.oldMacro),this.recording=!1):(this.$addCommandToMacro||(this.$addCommandToMacro=function(e){this.macro.push([e.command,e.args])}.bind(this)),this.oldMacro=this.macro,this.macro=[],this.on("exec",this.$addCommandToMacro),this.recording=!0)},this.replay=function(e){if(this.$inReplay||!this.macro)return;if(this.recording)return this.toggleRecording(e);try{this.$inReplay=!0,this.macro.forEach(function(t){typeof t=="string"?this.exec(t,e):this.exec(t[0],e,t[1])},this)}finally{this.$inReplay=!1}},this.trimMacro=function(e){return e.map(function(e){return typeof e[0]!="string"&&(e[0]=e[0].name),e[1]||(e=e[0]),e})}}.call(o.prototype),t.CommandManager=o});

Request (directory containing only the extract, format: "directory"):

{
  "detail": "summary",
  "source_paths": ["ace_1.8.1_min_command_manager.js"],
  "max_heap_mb": 1024,
  "analysis_timeout_ms": 60000,
  "max_semantic_nodes": 10,
  "max_semantic_nodes_per_file": 10,
  "max_semantic_unknowns": 10,
  "max_semantic_unknowns_per_file": 10
}

The tiny semantic retention limits do not prevent the OOM.

Why it may be worth a separate look. Unlike the update-queue drain, this module has no linked-list cursors (h=h.next). It does have:

  • methods written through this.* inside (function(){…}).call(o.prototype) after oop.inherits / oop.implement(this, EventEmitter)
  • a recursive this.exec over arrays, where a parameter is reassigned (e=this.commands[e])
  • this.macro / this.oldMacro being swapped
  • a .bind(this) callback that pushes into this.macro
  • this.macro.forEach(function(t){…this.exec…}, this)

None of these has been isolated as the cause yet.

Not tested against #8392. I have not run the draft branch on this input, so I can't say whether limiting widened joins covers it. Running it on this input should show whether this is the same growth path or a second one.

<!-- gh-comment-id:6591811688 --> @jgt87 commented on GitHub (Nov 3, 2026): **Another small public trigger, with a different shape: Ace 1.8.1 `ace/commands/command_manager` (1,796 bytes).** Same symptom on `rea-agents` 6.8.0 (latest on npm), Windows, through MCP: the worker exhausts its 1024 MiB old-space heap during `static`, with `parsed_javascript_files: 0`, `skipped_javascript_files: 1`, `visited_ast_nodes: 0`, `parse_failures: 0`. **Input.** The `ace.define("ace/commands/command_manager", …)` call taken from `ace-builds@1.8.1` (npm, BSD-3-Clause, integrity `sha512-wjEQ4khMQYg9FfdEDoOtqdoHwcwFL48H0VB3te5b5A3eqHwxsTw8IX6+xzfisgborIb8dYU+1y9tcmtGFrCPIg==`). It runs from `ace.define("ace/commands/command_manager"` up to the next `ace.define(`, with trailing `,`/`;`/whitespace trimmed and `;\n` appended. It is parsed only, never executed. | Variant | Source file | Bytes | SHA-256 | Result | |---|---|---|---|---| | minified | `src-min-noconflict/ace.js` | 1,796 | `eb5ad99736a082433ab0664bddbaacb94690a2681780f06e7c8169719a9ff6d1` | 1024 MiB heap exhausted in `static` | | readable | `src-noconflict/ace.js` | 3,233 | `c2c4fcc08a1d2680efeee0d85da077443fca37b4592f0f8b986ff91b4815beb0` | 1024 MiB heap exhausted in `static` | So minification is not the trigger. <details><summary>Minified input (1,796 bytes)</summary> ```js ace.define("ace/commands/command_manager",["require","exports","module","ace/lib/oop","ace/keyboard/hash_handler","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../keyboard/hash_handler").MultiHashHandler,s=e("../lib/event_emitter").EventEmitter,o=function(e,t){i.call(this,t,e),this.byName=this.commands,this.setDefaultHandler("exec",function(e){return e.args?e.command.exec(e.editor,e.args,e.event,!1):e.command.exec(e.editor,{},e.event,!0)})};r.inherits(o,i),function(){r.implement(this,s),this.exec=function(e,t,n){if(Array.isArray(e)){for(var r=e.length;r--;)if(this.exec(e[r],t,n))return!0;return!1}typeof e=="string"&&(e=this.commands[e]);if(!e)return!1;if(t&&t.$readOnly&&!e.readOnly)return!1;if(this.$checkCommandState!=0&&e.isAvailable&&!e.isAvailable(t))return!1;var i={editor:t,command:e,args:n};return i.returnValue=this._emit("exec",i),this._signal("afterExec",i),i.returnValue===!1?!1:!0},this.toggleRecording=function(e){if(this.$inReplay)return;return e&&e._emit("changeStatus"),this.recording?(this.macro.pop(),this.off("exec",this.$addCommandToMacro),this.macro.length||(this.macro=this.oldMacro),this.recording=!1):(this.$addCommandToMacro||(this.$addCommandToMacro=function(e){this.macro.push([e.command,e.args])}.bind(this)),this.oldMacro=this.macro,this.macro=[],this.on("exec",this.$addCommandToMacro),this.recording=!0)},this.replay=function(e){if(this.$inReplay||!this.macro)return;if(this.recording)return this.toggleRecording(e);try{this.$inReplay=!0,this.macro.forEach(function(t){typeof t=="string"?this.exec(t,e):this.exec(t[0],e,t[1])},this)}finally{this.$inReplay=!1}},this.trimMacro=function(e){return e.map(function(e){return typeof e[0]!="string"&&(e[0]=e[0].name),e[1]||(e=e[0]),e})}}.call(o.prototype),t.CommandManager=o}); ``` </details> **Request** (directory containing only the extract, `format: "directory"`): ```json { "detail": "summary", "source_paths": ["ace_1.8.1_min_command_manager.js"], "max_heap_mb": 1024, "analysis_timeout_ms": 60000, "max_semantic_nodes": 10, "max_semantic_nodes_per_file": 10, "max_semantic_unknowns": 10, "max_semantic_unknowns_per_file": 10 } ``` The tiny semantic retention limits do not prevent the OOM. **Why it may be worth a separate look.** Unlike the update-queue drain, this module has no linked-list cursors (`h=h.next`). It does have: - methods written through `this.*` inside `(function(){…}).call(o.prototype)` after `oop.inherits` / `oop.implement(this, EventEmitter)` - a recursive `this.exec` over arrays, where a parameter is reassigned (`e=this.commands[e]`) - `this.macro` / `this.oldMacro` being swapped - a `.bind(this)` callback that pushes into `this.macro` - `this.macro.forEach(function(t){…this.exec…}, this)` None of these has been isolated as the cause yet. **Not tested against #8392.** I have not run the draft branch on this input, so I can't say whether limiting widened joins covers it. Running it on this input should show whether this is the same growth path or a second one.
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#6531
No description provided.