Skip to content

Commit 08d24d4

Browse files
justin808claude
andcommitted
Improve memory debugging docs with simpler heap snapshot approach (#3072)
## Summary - Document `NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2"` as the recommended approach for capturing heap snapshots — a built-in Node flag that requires no code changes, versus the existing custom `process.on('SIGUSR2', ...)` handler approach - Add a step-by-step production container workflow for capturing snapshots from running deployments and copying them locally for analysis - Mention running the renderer as a separate workload for easier memory isolation during diagnosis Based on community discussion about practical memory debugging approaches in production. ## Changed files - `docs/pro/js-memory-leaks.md` — Split heap snapshot section into recommended (built-in flag) and detailed (custom handler) options; add production container workflow - `docs/oss/building-features/node-renderer/debugging.md` — Rewrite memory leaks section with simpler quick-start approach and isolation tip - `docs/pro/troubleshooting.md` — Add heap snapshot flag as first investigation step ## Test plan - [x] All links pass lychee checks (verified by pre-commit and pre-push hooks) - [x] Prettier formatting passes - [ ] Review rendered markdown for clarity 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: documentation-only updates that don’t change runtime behavior, but could mislead users if commands/flags are incorrect. > > **Overview** > **Improves memory-leak debugging guidance for the Node Renderer** by recommending Node’s built-in `NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2"` workflow to capture `.heapsnapshot` files without adding custom code. > > Adds clearer instructions for capturing and comparing snapshots in Chrome DevTools, documents a production container workflow (append to existing `NODE_OPTIONS`, find worker PIDs, signal multiple workers, copy snapshots out), and updates troubleshooting to point to this approach first; also notes that running the renderer as a separate workload can simplify isolation during diagnosis. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1f1c725. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3336bab commit 08d24d4

3 files changed

Lines changed: 72 additions & 15 deletions

File tree

docs/oss/building-features/node-renderer/debugging.md

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,43 @@ Use this when you need full control over the renderer process — different flag
5454

5555
## Debugging Memory Leaks
5656

57-
If worker memory grows over time, use heap snapshots to find the source:
57+
If worker memory grows over time, use heap snapshots to find the source.
5858

59-
1. Start the renderer with `--expose-gc` to enable forced GC before snapshots:
60-
```bash
61-
cd react_on_rails_pro/spec/dummy
62-
# Adjust the port if your Rails app points at a different renderer URL.
63-
RENDERER_PORT=3800 node --expose-gc client/node-renderer.js
64-
```
65-
2. Take heap snapshots at different times using `v8.writeHeapSnapshot()` (triggered via `SIGUSR2` signal or a custom endpoint).
66-
3. Load both snapshots in Chrome DevTools (Memory tab → Load) and use the **Comparison** view to see which objects accumulated between snapshots.
67-
4. Look for growing `string`, `Object`, and `Array` counts — these typically point to module-level caches.
59+
### Quick approach: `--heapsnapshot-signal` (no code changes)
60+
61+
Use Node's built-in flag to write heap snapshots on demand:
62+
63+
```bash
64+
cd react_on_rails_pro/spec/dummy
65+
# Adjust the port if your Rails app points at a different renderer URL.
66+
NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2" RENDERER_PORT=3800 node client/node-renderer.js
67+
```
68+
69+
Then capture snapshots at different times:
70+
71+
```bash
72+
kill -USR2 <worker-pid> # writes a .heapsnapshot file to the working directory
73+
```
74+
75+
This also works in production containers — set `NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2"` as an environment variable, send the signal at different times, then copy the `.heapsnapshot` files to your local machine for analysis.
76+
77+
### Detailed approach: with forced GC
78+
79+
For more precise results, start the renderer with `--expose-gc` and a custom signal handler that forces garbage collection before each snapshot. See the [Memory Leaks guide](../../../pro/js-memory-leaks.md#2-take-v8-heap-snapshots) for the code.
80+
81+
### Analyzing snapshots
82+
83+
1. Load both `.heapsnapshot` files in Chrome DevTools (Memory tab → Load).
84+
2. Use the **Comparison** view to see which objects accumulated between snapshots.
85+
3. Look for growing `string`, `Object`, and `Array` counts — these typically point to module-level caches.
86+
87+
### Isolating memory issues with a separate renderer workload
88+
89+
When diagnosing memory leaks in a containerized environment, running the Node renderer as a separate workload (instead of a sidecar) makes it easier to:
90+
91+
- Monitor Node memory independently from Rails
92+
- Capture heap snapshots without affecting the Rails process
93+
- Restart or scale the renderer without restarting Rails
6894

6995
See the [Memory Leaks guide](../../../pro/js-memory-leaks.md) for common patterns and fixes.
7096

docs/pro/js-memory-leaks.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,20 +136,50 @@ done
136136

137137
### 2. Take V8 heap snapshots
138138

139-
Use `v8.writeHeapSnapshot()` to capture heap state before and after load, then compare in Chrome DevTools:
139+
#### Option A: Built-in `--heapsnapshot-signal` (recommended)
140+
141+
Node provides a built-in flag that writes heap snapshots on a signal — no custom code required:
142+
143+
```bash
144+
NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2" node node-renderer.js
145+
```
146+
147+
Then send `kill -USR2 <worker-pid>` at different times to capture snapshots. Each signal writes a `.heapsnapshot` file to the working directory.
148+
149+
This is the simplest approach, especially for production containers where you don't want to modify application code.
150+
151+
#### Option B: Custom signal handler
152+
153+
> **Note:** If you are already using Option A (`--heapsnapshot-signal=SIGUSR2`), do not also register a `process.on('SIGUSR2', ...)` handler — both will fire on every signal, producing duplicate snapshots. Remove one before using the other.
154+
155+
If you need more control (e.g., forced GC before the snapshot, custom filenames, or writing to a specific directory), add a custom handler:
140156

141157
```javascript
142-
// In your renderer config, add a way to trigger snapshots:
143158
const v8 = require('v8');
144159

145160
process.on('SIGUSR2', () => {
146-
if (global.gc) global.gc(); // force GC first
161+
if (global.gc) global.gc(); // force GC first (requires --expose-gc)
147162
const filename = v8.writeHeapSnapshot();
148163
console.log(`Heap snapshot written to ${filename}`);
149164
});
150165
```
151166

152-
Then send `kill -USR2 <worker-pid>` at different times and compare the snapshots in Chrome DevTools (Memory tab → Load).
167+
Then send `kill -USR2 <worker-pid>` at different times.
168+
169+
#### Comparing snapshots
170+
171+
Load both `.heapsnapshot` files in Chrome DevTools (Memory tab → Load) and use the **Comparison** view to see which objects accumulated between snapshots.
172+
173+
#### Production container workflow
174+
175+
To diagnose leaks in a running container:
176+
177+
1. Set `NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2"` in the container environment. If `NODE_OPTIONS` is already set (e.g., `--max-old-space-size=1536`), append the flag: `NODE_OPTIONS="--max-old-space-size=1536 --heapsnapshot-signal=SIGUSR2"`.
178+
2. Identify the worker PIDs: `ps aux | grep node` inside the container. In a multi-worker setup you'll see one PID per worker — signal each one separately to get a snapshot from each process.
179+
3. Capture a baseline snapshot: `kill -USR2 <worker-pid>`.
180+
4. Wait for traffic to accumulate (e.g., 10–30 minutes), then capture another: `kill -USR2 <worker-pid>`.
181+
5. Copy the `.heapsnapshot` files from the container to your local machine (e.g., `kubectl cp`, `docker cp`, or `cpln workload exec`).
182+
6. Compare the two snapshots in Chrome DevTools to identify what grew between them.
153183

154184
### 3. Use `--inspect` for live profiling
155185

docs/pro/troubleshooting.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ For issues related to upgrading from GitHub Packages to public distribution, see
3535

3636
**Investigation**:
3737

38-
- Profile memory using `node --inspect` and heap snapshots (see [Profiling guide](./profiling-server-side-rendering-code.md))
38+
- Capture heap snapshots with `NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2"` — send `kill -USR2 <worker-pid>` at different times and compare in Chrome DevTools (see [Debugging guide](../oss/building-features/node-renderer/debugging.md#debugging-memory-leaks))
39+
- Profile memory using `node --inspect` and Chrome DevTools (see [Profiling guide](./profiling-server-side-rendering-code.md))
3940
- Search your server bundle code for module-level `Map`, `Set`, `{}` caches, and `_.memoize` calls — these are the most common leak sources
4041
- Use `config.ssr_pre_hook_js` to run cleanup code before each render (e.g., clearing global state)
4142
- See the [Memory Leaks guide](./js-memory-leaks.md) for detailed patterns, an audit checklist, and fixes

0 commit comments

Comments
 (0)