mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user