mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
feat(cli): add --heap-prof and --heap-prof-text flags for heap profiling (#26326)
## Summary
- Add `--heap-prof` CLI flag for generating V8-compatible heap snapshots
(`.heapsnapshot`)
- Add `--heap-prof-md` CLI flag for generating markdown heap profiles
(`.md`) designed for CLI analysis
- Add `--heap-prof-name` and `--heap-prof-dir` options for customizing
output location
## Usage
```bash
# Generate V8-compatible heap snapshot (opens in Chrome DevTools)
bun --heap-prof script.js
# Generate markdown heap profile (for CLI analysis with grep/sed/awk)
bun --heap-prof-md script.js
# Specify output location
bun --heap-prof --heap-prof-dir ./profiles --heap-prof-name my-snapshot.heapsnapshot script.js
```
## Example Output (`--heap-prof-md`)
<details>
<summary>Click to expand example markdown profile</summary>
```markdown
# Bun Heap Profile
Generated by `bun --heap-prof-md`. This profile contains complete heap data in markdown format.
**Quick Search Commands:**
```bash
grep 'type=Function' file.md # Find all Function objects
grep 'size=[0-9]\{5,\}' file.md # Find objects >= 10KB
grep 'EDGE.*to=12345' file.md # Find references to object #12345
grep 'gcroot=1' file.md # Find all GC roots
```
---
## Summary
| Metric | Value |
|--------|------:|
| Total Heap Size | 208.2 KB (213265 bytes) |
| Total Objects | 2651 |
| Total Edges | 7337 |
| Unique Types | 73 |
| GC Roots | 426 |
## Top 50 Types by Retained Size
| Rank | Type | Count | Self Size | Retained Size | Largest Instance |
|-----:|------|------:|----------:|--------------:|-----------------:|
| 1 | `Function` | 568 | 18.7 KB | 5.4 MB | 10.4 KB |
| 2 | `Structure` | 247 | 27.0 KB | 2.0 MB | 10.4 KB |
| 3 | `FunctionExecutable` | 306 | 38.2 KB | 375.5 KB | 13.0 KB |
| 4 | `FunctionCodeBlock` | 25 | 21.5 KB | 294.1 KB | 14.0 KB |
| 5 | `string` | 591 | 11.3 KB | 75.9 KB | 177 B |
...
## Top 50 Largest Objects
Objects that retain the most memory (potential memory leak sources):
| Rank | ID | Type | Self Size | Retained Size | Out-Edges | In-Edges |
|-----:|---:|------|----------:|--------------:|----------:|---------:|
| 1 | 0 | `<root>` | 0 B | 58.1 KB | 852 | 0 |
| 2 | 774 | `GlobalObject` | 10.0 KB | 41.9 KB | 717 | 807 |
| 3 | 600 | `ModuleProgramCodeBlock` | 1.2 KB | 23.9 KB | 30 | 1 |
...
## Retainer Chains
How the top 20 largest objects are kept alive (path from GC root to object):
### 1. Object #0 - `<root>` (58.1 KB retained)
```
(no path to GC root found)
```
### 2. Object #774 - `GlobalObject` (41.9 KB retained)
```
GlobalObject#774 [ROOT] (this object is a GC root)
```
...
## GC Roots
| ID | Type | Size | Retained | Label |
|---:|------|-----:|---------:|-------|
| 0 | `<root>` | 0 B | 58.1 KB | |
| 774 | `GlobalObject` | 10.0 KB | 41.9 KB | |
...
<details>
<summary>Click to expand 2651 objects (searchable with grep)</summary>
| ID | Type | Size | Retained | Flags | Label |
|---:|------|-----:|---------:|-------|-------|
| 0 | `<root>` | 0 | 59467 | gcroot=1 | |
| 1 | `Structure` | 112 | 10644 | | |
...
</details>
```
</details>
## Test plan
- [x] `bun bd test test/cli/heap-prof.test.ts` - All 7 tests pass
- [x] `USE_SYSTEM_BUN=1 bun test test/cli/heap-prof.test.ts` - Tests
fail (feature not in system bun)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
233
test/cli/heap-prof.test.ts
Normal file
233
test/cli/heap-prof.test.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
const testScript = `const arr = []; for (let i = 0; i < 100; i++) arr.push({ x: i, y: "hello" + i }); console.log("done");`;
|
||||
|
||||
test("--heap-prof generates V8 heap snapshot on exit", async () => {
|
||||
using dir = tempDir("heap-prof-v8-test", {});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--heap-prof", "-e", testScript],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("done");
|
||||
expect(stderr).toContain("Heap profile written to:");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Find the heap snapshot file (V8 format)
|
||||
const glob = new Bun.Glob("Heap.*.heapsnapshot");
|
||||
const files = Array.from(glob.scanSync({ cwd: String(dir) }));
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
|
||||
// Read and validate the heap snapshot content (should be valid JSON in V8 format)
|
||||
const profilePath = join(String(dir), files[0]);
|
||||
const content = await Bun.file(profilePath).text();
|
||||
|
||||
// V8 heap snapshot format is JSON with specific structure
|
||||
const snapshot = JSON.parse(content);
|
||||
expect(snapshot).toHaveProperty("snapshot");
|
||||
expect(snapshot).toHaveProperty("nodes");
|
||||
expect(snapshot).toHaveProperty("edges");
|
||||
expect(snapshot).toHaveProperty("strings");
|
||||
});
|
||||
|
||||
test("--heap-prof-md generates markdown heap profile on exit", async () => {
|
||||
using dir = tempDir("heap-prof-md-test", {});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--heap-prof-md", "-e", testScript],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("done");
|
||||
expect(stderr).toContain("Heap profile written to:");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Find the heap profile file (markdown format)
|
||||
const glob = new Bun.Glob("Heap.*.md");
|
||||
const files = Array.from(glob.scanSync({ cwd: String(dir) }));
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
|
||||
// Read and validate the heap profile content
|
||||
const profilePath = join(String(dir), files[0]);
|
||||
const content = await Bun.file(profilePath).text();
|
||||
|
||||
// Check for markdown headers
|
||||
expect(content).toContain("# Bun Heap Profile");
|
||||
expect(content).toContain("## Summary");
|
||||
expect(content).toContain("## Top 50 Types by Retained Size");
|
||||
expect(content).toContain("## Top 50 Largest Objects");
|
||||
expect(content).toContain("## Retainer Chains");
|
||||
expect(content).toContain("## GC Roots");
|
||||
|
||||
// Check for summary table structure
|
||||
expect(content).toContain("| Metric | Value |");
|
||||
expect(content).toContain("| Total Heap Size |");
|
||||
expect(content).toContain("| Total Objects |");
|
||||
expect(content).toContain("| Unique Types |");
|
||||
expect(content).toContain("| GC Roots |");
|
||||
|
||||
// Check for table structure in types section
|
||||
expect(content).toContain("| Rank | Type | Count | Self Size | Retained Size |");
|
||||
|
||||
// Check for collapsible sections
|
||||
expect(content).toContain("<details>");
|
||||
expect(content).toContain("<summary>");
|
||||
|
||||
// Check for All Objects table format
|
||||
expect(content).toContain("## All Objects");
|
||||
expect(content).toContain("| ID | Type | Size | Retained | Flags | Label |");
|
||||
|
||||
// Check for All Edges table format
|
||||
expect(content).toContain("## All Edges");
|
||||
expect(content).toContain("| From | To | Type | Name |");
|
||||
|
||||
// Check for Type Statistics table format
|
||||
expect(content).toContain("## Complete Type Statistics");
|
||||
expect(content).toContain("| Type | Count | Self Size | Retained Size | Largest ID |");
|
||||
});
|
||||
|
||||
test("--heap-prof-dir specifies output directory for V8 format", async () => {
|
||||
using dir = tempDir("heap-prof-dir-test", {
|
||||
"profiles": {},
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--heap-prof", "--heap-prof-dir", "profiles", "-e", `console.log("hello");`],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("hello");
|
||||
expect(stderr).toContain("Heap profile written to:");
|
||||
// Check for "profiles" directory in path (handles both / and \ separators)
|
||||
expect(stderr).toMatch(/profiles[/\\]/);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check the profile is in the specified directory
|
||||
const glob = new Bun.Glob("Heap.*.heapsnapshot");
|
||||
const files = Array.from(glob.scanSync({ cwd: join(String(dir), "profiles") }));
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("--heap-prof-dir specifies output directory for markdown format", async () => {
|
||||
using dir = tempDir("heap-prof-md-dir-test", {
|
||||
"profiles": {},
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--heap-prof-md", "--heap-prof-dir", "profiles", "-e", `console.log("hello");`],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("hello");
|
||||
expect(stderr).toContain("Heap profile written to:");
|
||||
// Check for "profiles" directory in path (handles both / and \ separators)
|
||||
expect(stderr).toMatch(/profiles[/\\]/);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check the profile is in the specified directory
|
||||
const glob = new Bun.Glob("Heap.*.md");
|
||||
const files = Array.from(glob.scanSync({ cwd: join(String(dir), "profiles") }));
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("--heap-prof-name specifies output filename", async () => {
|
||||
using dir = tempDir("heap-prof-name-test", {});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--heap-prof", "--heap-prof-name", "my-profile.heapsnapshot", "-e", `console.log("hello");`],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("hello");
|
||||
expect(stderr).toContain("Heap profile written to:");
|
||||
expect(stderr).toContain("my-profile.heapsnapshot");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check the profile exists with the specified name
|
||||
const profilePath = join(String(dir), "my-profile.heapsnapshot");
|
||||
expect(Bun.file(profilePath).size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("--heap-prof-name and --heap-prof-dir work together", async () => {
|
||||
using dir = tempDir("heap-prof-both-test", {
|
||||
"output": {},
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"--heap-prof",
|
||||
"--heap-prof-dir",
|
||||
"output",
|
||||
"--heap-prof-name",
|
||||
"custom.heapsnapshot",
|
||||
"-e",
|
||||
`console.log("hello");`,
|
||||
],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("hello");
|
||||
expect(stderr).toContain("Heap profile written to:");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check the profile exists in the specified location
|
||||
const profilePath = join(String(dir), "output", "custom.heapsnapshot");
|
||||
expect(Bun.file(profilePath).size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("--heap-prof-name without --heap-prof or --heap-prof-md shows warning", async () => {
|
||||
using dir = tempDir("heap-prof-warn-test", {});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--heap-prof-name", "test.heapsnapshot", "-e", `console.log("hello");`],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("hello");
|
||||
expect(stderr).toContain("--heap-prof-name requires --heap-prof or --heap-prof-md to be enabled");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// No profile should be generated
|
||||
const glob = new Bun.Glob("*.heap*");
|
||||
const files = Array.from(glob.scanSync({ cwd: String(dir) }));
|
||||
expect(files.length).toBe(0);
|
||||
});
|
||||
@@ -7,7 +7,7 @@
|
||||
".arguments_old(": 263,
|
||||
".jsBoolean(false)": 0,
|
||||
".jsBoolean(true)": 0,
|
||||
".stdDir()": 42,
|
||||
".stdDir()": 41,
|
||||
".stdFile()": 16,
|
||||
"// autofix": 148,
|
||||
": [^=]+= undefined,$": 256,
|
||||
|
||||
Reference in New Issue
Block a user