Files
bun.sh/test/cli/heap-prof.test.ts
robobun 3b1c3bfe97 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>
2026-01-22 15:27:37 -08:00

234 lines
8.0 KiB
TypeScript

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);
});