feat(cli): add --cpu-prof-md flag for markdown CPU profile output (#26327)

## Summary
- Adds `--cpu-prof-md` flag that outputs CPU profiling data in markdown
format optimized for GitHub rendering and LLM analysis
- Complements the existing `--cpu-prof` flag which outputs Chrome
DevTools JSON format
- `--cpu-prof-md` works standalone or combined with `--cpu-prof` to
generate both formats

## Usage
```bash
# Markdown only
bun --cpu-prof-md script.js

# Both formats
bun --cpu-prof --cpu-prof-md script.js
```

## Example Output

# CPU Profile

| Duration | Samples | Interval | Functions |
|----------|---------|----------|----------|
| 255.7ms | 178 | 1ms | 32 |

**Top 10:** \`fibonacci\` 23.6%, \`fibonacci\` 12.6%, \`parseModule\`
11.7%, \`(anonymous)\` 9.5%, \`loadAndEvaluateModule\` 5.5%,
\`requestSatisfyUtil\` 3.7%, \`main\` 2.7%,
\`moduleDeclarationInstantiation\` 2.6%, \`loadModule\` 2.5%,
\`cacheSatisfyAndReturn\` 2.5%

## Hot Functions (Self Time)

| Self% | Self | Total% | Total | Function | Location |
|------:|-----:|-------:|------:|----------|----------|
| 23.6% | 60.5ms | 23.6% | 60.5ms | \`fibonacci\` | /tmp/test-profile.js
|
| 12.6% | 32.3ms | 100.0% | 1.29s | \`fibonacci\` |
/tmp/test-profile.js:3 |
| 11.7% | 29.9ms | 11.7% | 29.9ms | \`parseModule\` | [native code] |
| 9.5% | 24.3ms | 43.4% | 111.0ms | \`(anonymous)\` | [native code] |
| 5.5% | 14.2ms | 99.9% | 255.5ms | \`loadAndEvaluateModule\` | [native
code] |

## Call Tree (Total Time)

| Total% | Total | Self% | Self | Function | Location |
|-------:|------:|------:|-----:|----------|----------|
| 100.0% | 1.29s | 12.6% | 32.3ms | \`fibonacci\` |
/tmp/test-profile.js:3 |
| 99.9% | 255.5ms | 5.5% | 14.2ms | \`loadAndEvaluateModule\` | [native
code] |
| 86.0% | 219.9ms | 1.3% | 3.3ms | \`moduleEvaluation\` | [native code]
|
| 43.4% | 111.0ms | 9.5% | 24.3ms | \`(anonymous)\` | [native code] |

## Function Details

### \`fibonacci\`

- **Location:** \`/tmp/test-profile.js:3\`
- **Self:** 12.6% (32.3ms) | **Total:** 100.0% (1.29s)
- **Called by:** \`fibonacci\` (864), \`main\` (68)
- **Calls:** \`fibonacci\` (864), \`fibonacci\` (44), \`fibonacci\` (2)

### \`main\`

- **Location:** \`/tmp/test-profile.js:9\`
- **Self:** 0.0% (0us) | **Total:** 38.4% (98.2ms)
- **Called by:** \`(module)\` (72)
- **Calls:** \`fibonacci\` (68), \`inspect\` (2), \`fibonacci\` (2)

## Files

| Self% | Self | File |
|------:|-----:|------|
| 58.8% | 150.6ms | \`[native code]\` |
| 40.1% | 102.6ms | \`/tmp/test-profile.js\` |
| 0.9% | 2.4ms | \`bun:main\` |

## Test plan
- [x] `--cpu-prof-md` generates `.md` file with markdown tables
- [x] `--cpu-prof-md` works standalone without `--cpu-prof`
- [x] Both flags together generate both `.cpuprofile` and `.md` files
- [x] Custom filename with `--cpu-prof-name` works
- [x] Custom directory with `--cpu-prof-dir` works
- [x] All 9 tests pass

🤖 Generated with [Claude Code](https://claude.ai/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>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
robobun
2026-01-21 13:21:01 -08:00
committed by GitHub
parent b6b3626c14
commit 2febdb5b49
8 changed files with 1054 additions and 32 deletions

View File

@@ -187,4 +187,221 @@ describe.concurrent("--cpu-prof", () => {
expect(functionNames.some((name: string) => name !== "(root)" && name !== "(program)")).toBe(true);
expect(exitCode).toBe(0);
});
test("--cpu-prof-md generates markdown format profile", async () => {
using dir = tempDir("cpu-prof-md", {
"test.js": `
// CPU-intensive task for text profile
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
function main() {
const now = performance.now();
while (now + 50 > performance.now()) {
Bun.inspect(fibonacci(20));
}
}
main();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", "--cpu-prof-md", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
// Check that a .md file was created (not .cpuprofile)
const files = readdirSync(String(dir));
const mdFiles = files.filter(f => f.endsWith(".md") && f.startsWith("CPU."));
expect(mdFiles.length).toBeGreaterThan(0);
expect(exitCode).toBe(0);
// Read and validate the markdown profile format
const profilePath = join(String(dir), mdFiles[0]);
const profileContent = readFileSync(profilePath, "utf-8");
// Validate the markdown format has expected sections
expect(profileContent).toContain("# CPU Profile");
expect(profileContent).toContain("## Hot Functions (Self Time)");
expect(profileContent).toContain("## Call Tree (Total Time)");
expect(profileContent).toContain("## Function Details");
expect(profileContent).toContain("## Files");
// Validate header contains summary info in markdown table
expect(profileContent).toMatch(/\| Duration \| Samples \| Interval \| Functions \|/);
// Validate function details have caller/callee info
expect(profileContent).toContain("**Called by:**");
expect(profileContent).toContain("**Calls:**");
});
test("--cpu-prof-md with custom name", async () => {
using dir = tempDir("cpu-prof-md-name", {
"test.js": `
function loop() {
const end = Date.now() + 32;
while (Date.now() < end) {}
}
loop();
`,
});
const customName = "my-profile.md";
// --cpu-prof-md works standalone, no need for --cpu-prof
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof-md", "--cpu-prof-name", customName, "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
const files = readdirSync(String(dir));
expect(files).toContain(customName);
expect(exitCode).toBe(0);
// Validate it's markdown format
const profileContent = readFileSync(join(String(dir), customName), "utf-8");
expect(profileContent).toContain("# CPU Profile");
});
test("--cpu-prof-md shows function details with relationships", async () => {
using dir = tempDir("cpu-prof-md-details", {
"test.js": `
function workA() {
let sum = 0;
for (let i = 0; i < 500000; i++) sum += i;
return sum;
}
function workB() {
let sum = 0;
for (let i = 0; i < 500000; i++) sum += i;
return sum;
}
function main() {
const now = performance.now();
while (now + 50 > performance.now()) {
workA();
workB();
}
}
main();
`,
});
// --cpu-prof-md works standalone
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof-md", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const files = readdirSync(String(dir));
const mdFiles = files.filter(f => f.endsWith(".md") && f.startsWith("CPU."));
expect(mdFiles.length).toBeGreaterThan(0);
const profileContent = readFileSync(join(String(dir), mdFiles[0]), "utf-8");
// Check markdown sections
expect(profileContent).toMatch(/## Hot Functions \(Self Time\)/);
expect(profileContent).toMatch(/## Call Tree \(Total Time\)/);
expect(profileContent).toMatch(/## Function Details/);
expect(profileContent).toMatch(/## Files/);
// Check function detail headers (### `functionName`)
expect(profileContent).toMatch(/^### `/m);
});
test("--cpu-prof-md works standalone without --cpu-prof", async () => {
using dir = tempDir("cpu-prof-md-standalone", {
"test.js": `
function loop() {
const end = Date.now() + 32;
while (Date.now() < end) {}
}
loop();
`,
});
// Use ONLY --cpu-prof-md without --cpu-prof
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof-md", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
// Check that a .md file was created
const files = readdirSync(String(dir));
const mdFiles = files.filter(f => f.endsWith(".md") && f.startsWith("CPU."));
expect(mdFiles.length).toBeGreaterThan(0);
expect(exitCode).toBe(0);
// Validate it's the markdown format
const profileContent = readFileSync(join(String(dir), mdFiles[0]), "utf-8");
expect(profileContent).toContain("# CPU Profile");
});
test("--cpu-prof and --cpu-prof-md together creates both files", async () => {
using dir = tempDir("cpu-prof-both-formats", {
"test.js": `
function loop() {
const end = Date.now() + 32;
while (Date.now() < end) {}
}
loop();
`,
});
// Use both flags together
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", "--cpu-prof-md", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
// Check that both .cpuprofile and .md files were created
const files = readdirSync(String(dir));
const jsonFiles = files.filter(f => f.endsWith(".cpuprofile"));
const mdFiles = files.filter(f => f.endsWith(".md") && f.startsWith("CPU."));
expect(jsonFiles.length).toBeGreaterThan(0);
expect(mdFiles.length).toBeGreaterThan(0);
expect(exitCode).toBe(0);
// Validate JSON file
const jsonContent = readFileSync(join(String(dir), jsonFiles[0]), "utf-8");
const profile = JSON.parse(jsonContent);
expect(profile).toHaveProperty("nodes");
expect(profile).toHaveProperty("samples");
// Validate markdown file
const mdContent = readFileSync(join(String(dir), mdFiles[0]), "utf-8");
expect(mdContent).toContain("# CPU Profile");
});
});