Skip to content

Commit 74c4ac8

Browse files
authored
Add pprof-to-md integration for LLM-friendly markdown analysis (#58)
Integrates pprof-to-md to generate markdown analysis files alongside existing HTML flamegraphs and pb files, making profile data more accessible to LLMs and automated analysis tools. Changes: - Added pprof-to-md dependency and bumped Node.js requirement to >=22.6.0 - Implemented generateMarkdown() function in lib/index.js - Extended preload.js to generate markdown files automatically - Added --md-format CLI flag supporting summary/detailed/adaptive modes - Added comprehensive test coverage (8 new tests across unit/integration) - Updated documentation with markdown generation examples and usage The markdown format provides structured, readable profile analysis that can be easily consumed by LLMs, while maintaining full backward compatibility with existing HTML and pb output formats.
1 parent 57b6f8c commit 74c4ac8

File tree

8 files changed

+344
-21
lines changed

8 files changed

+344
-21
lines changed

README.md

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- **Dual Profiling**: Captures both CPU and heap profiles concurrently for comprehensive performance insights
88
- **Auto-Start Profiling**: Profiling starts immediately when using `flame run` (default behavior)
99
- **Automatic Flamegraph Generation**: Interactive HTML flamegraphs are created automatically for both CPU and heap profiles on exit
10+
- **LLM-Friendly Markdown Analysis**: Generates markdown reports with hotspot analysis, ideal for AI-assisted performance debugging
1011
- **Sourcemap Support**: Automatically translates transpiled code locations back to original source files (TypeScript, bundled JavaScript, etc.)
1112
- **Clear File Path Display**: Shows exact paths and browser URLs for generated files
1213
- **Manual Control**: Optional manual mode with signal-based control using `SIGUSR2`
@@ -34,10 +35,11 @@ flame run server.js
3435
# 🔥 Heap profile written to: heap-profile-2025-08-27T12-00-00-000Z.pb
3536
# 🔥 Generating CPU flamegraph...
3637
# 🔥 CPU flamegraph generated: cpu-profile-2025-08-27T12-00-00-000Z.html
38+
# 🔥 CPU markdown generated: cpu-profile-2025-08-27T12-00-00-000Z.md
3739
# 🔥 Generating heap flamegraph...
3840
# 🔥 Heap flamegraph generated: heap-profile-2025-08-27T12-00-00-000Z.html
41+
# 🔥 Heap markdown generated: heap-profile-2025-08-27T12-00-00-000Z.md
3942
# 🔥 Open file:///path/to/cpu-profile-2025-08-27T12-00-00-000Z.html in your browser to view the CPU flamegraph
40-
# 🔥 Open file:///path/to/heap-profile-2025-08-27T12-00-00-000Z.html in your browser to view the heap flamegraph
4143
```
4244

4345
### Manual Profiling Mode
@@ -53,14 +55,17 @@ kill -USR2 <PID>
5355
flame toggle
5456
```
5557

56-
### Generate Flamegraph
58+
### Generate Flamegraph and Markdown
5759

5860
```bash
59-
# Generate HTML flamegraph from pprof file
61+
# Generate HTML flamegraph and markdown from pprof file
6062
flame generate cpu-profile-2024-01-01T12-00-00-000Z.pb
6163

6264
# Specify custom output file
6365
flame generate -o my-flamegraph.html profile.pb.gz
66+
67+
# Use detailed markdown format for comprehensive analysis
68+
flame generate --md-format=detailed profile.pb
6469
```
6570

6671
## CLI Usage
@@ -70,14 +75,15 @@ flame [options] <command>
7075

7176
Commands:
7277
run <script> Run a script with profiling enabled
73-
generate <pprof-file> Generate HTML flamegraph from pprof file
78+
generate <pprof-file> Generate HTML flamegraph and markdown from pprof file
7479
toggle Toggle profiling for running flame processes
7580

7681
Options:
7782
-o, --output <file> Output HTML file (for generate command)
7883
-m, --manual Manual profiling mode (require SIGUSR2 to start)
7984
-d, --delay <value> Delay before starting profiler (ms, 'none', or 'until-started')
8085
-s, --sourcemap-dirs <dirs> Directories to search for sourcemaps (colon/semicolon-separated)
86+
--md-format <format> Markdown format: summary (default), detailed, or adaptive
8187
--node-options <options> Node.js CLI options to pass to the profiled process
8288
-h, --help Show help message
8389
-v, --version Show version number
@@ -86,7 +92,7 @@ Options:
8692
## Programmatic API
8793

8894
```javascript
89-
const { startProfiling, generateFlamegraph, parseProfile } = require('@platformatic/flame')
95+
const { startProfiling, generateFlamegraph, generateMarkdown, parseProfile } = require('@platformatic/flame')
9096

9197
// Start profiling a script with auto-start (default)
9298
const { pid, toggleProfiler } = startProfiling('server.js', ['--port', '3000'], { autoStart: true })
@@ -102,16 +108,20 @@ toggleProfiler()
102108
// Generate interactive flamegraph from pprof file
103109
await generateFlamegraph('profile.pb.gz', 'flamegraph.html')
104110

111+
// Generate LLM-friendly markdown analysis
112+
await generateMarkdown('profile.pb', 'analysis.md', { format: 'summary' })
113+
105114
// Parse profile data
106115
const profile = await parseProfile('profile.pb')
107116
```
108117

109118
## How It Works
110119

111120
1. **Auto-Start Mode (Default)**: Both CPU and heap profiling begin immediately when `flame run` starts your script
112-
2. **Auto-Generation on Exit**: Profile (.pb) files and interactive HTML flamegraphs are automatically created for both CPU and heap profiles when the process exits
121+
2. **Auto-Generation on Exit**: Profile (.pb) files, interactive HTML flamegraphs, and markdown analysis files are automatically created for both CPU and heap profiles when the process exits
113122
3. **Manual Mode**: Use `--manual` flag to require `SIGUSR2` signals for start/stop control (no auto-HTML generation)
114123
4. **Interactive Visualization**: The `@platformatic/react-pprof` library generates interactive WebGL-based HTML flamegraphs for both profile types
124+
5. **Markdown Analysis**: The `pprof-to-md` library generates LLM-friendly markdown reports with hotspot tables for AI-assisted debugging
115125

116126
## Sourcemap Support
117127

@@ -167,11 +177,23 @@ flame run server.js
167177

168178
Profile files are saved with timestamps in the format:
169179
```
170-
cpu-profile-2024-01-01T12-00-00-000Z.pb
180+
cpu-profile-2024-01-01T12-00-00-000Z.pb # Binary pprof data
181+
cpu-profile-2024-01-01T12-00-00-000Z.html # Interactive flamegraph
182+
cpu-profile-2024-01-01T12-00-00-000Z.md # LLM-friendly markdown analysis
171183
heap-profile-2024-01-01T12-00-00-000Z.pb
184+
heap-profile-2024-01-01T12-00-00-000Z.html
185+
heap-profile-2024-01-01T12-00-00-000Z.md
172186
```
173187

174-
Both CPU and heap profiles share the same timestamp for easy correlation. The files are compressed Protocol Buffer format compatible with the pprof ecosystem.
188+
Both CPU and heap profiles share the same timestamp for easy correlation. The `.pb` files are compressed Protocol Buffer format compatible with the pprof ecosystem. The `.md` files contain hotspot analysis tables suitable for AI/LLM-assisted performance debugging.
189+
190+
### Markdown Formats
191+
192+
The `--md-format` option controls the markdown output:
193+
194+
- **summary** (default): Compact hotspots table, ideal for AI triage and quick overview
195+
- **detailed**: Comprehensive analysis with full stack traces and detailed statistics
196+
- **adaptive**: Automatically chooses format based on profile complexity
175197

176198
## Integration with Existing Apps
177199

@@ -200,14 +222,15 @@ curl http://localhost:3000
200222
curl http://localhost:3000
201223
curl http://localhost:3000
202224

203-
# Stop the server (Ctrl-C) to automatically save profiles and generate HTML flamegraphs
225+
# Stop the server (Ctrl-C) to automatically save profiles and generate flamegraphs
204226
# You'll see the exact file paths and browser URLs in the output:
205227
# 🔥 CPU profile written to: cpu-profile-2025-08-27T15-30-45-123Z.pb
206228
# 🔥 Heap profile written to: heap-profile-2025-08-27T15-30-45-123Z.pb
207229
# 🔥 CPU flamegraph generated: cpu-profile-2025-08-27T15-30-45-123Z.html
230+
# 🔥 CPU markdown generated: cpu-profile-2025-08-27T15-30-45-123Z.md
208231
# 🔥 Heap flamegraph generated: heap-profile-2025-08-27T15-30-45-123Z.html
232+
# 🔥 Heap markdown generated: heap-profile-2025-08-27T15-30-45-123Z.md
209233
# 🔥 Open file:///path/to/cpu-profile-2025-08-27T15-30-45-123Z.html in your browser to view the CPU flamegraph
210-
# 🔥 Open file:///path/to/heap-profile-2025-08-27T15-30-45-123Z.html in your browser to view the heap flamegraph
211234
```
212235

213236
**Manual Mode:**
@@ -296,9 +319,10 @@ kill -USR2 <PID>
296319

297320
## Requirements
298321

299-
- Node.js >= 18.0.0
300-
- `@datadog/pprof` for CPU profiling
301-
- `@platformatic/react-pprof` for flamegraph generation
322+
- Node.js >= 22.6.0
323+
- `@datadog/pprof` for CPU and heap profiling
324+
- `react-pprof` for flamegraph generation
325+
- `pprof-to-md` for markdown analysis generation
302326

303327
## License
304328

bin/flame.js

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const { parseArgs } = require('node:util')
44
const fs = require('fs')
55
const path = require('path')
6-
const { startProfiling, generateFlamegraph } = require('../lib/index.js')
6+
const { startProfiling, generateFlamegraph, generateMarkdown } = require('../lib/index.js')
77

88
const { values: args, positionals } = parseArgs({
99
args: process.argv.slice(2),
@@ -42,6 +42,9 @@ const { values: args, positionals } = parseArgs({
4242
'node-modules-source-maps': {
4343
type: 'string',
4444
short: 'n'
45+
},
46+
'md-format': {
47+
type: 'string'
4548
}
4649
},
4750
allowPositionals: true
@@ -59,7 +62,7 @@ Usage: flame [options] <command>
5962
6063
Commands:
6164
run <script> Run a script with profiling enabled
62-
generate <pprof-file> Generate HTML flamegraph from pprof file
65+
generate <pprof-file> Generate HTML flamegraph and markdown from pprof file
6366
6467
Options:
6568
-o, --output <file> Output HTML file (for generate command)
@@ -69,6 +72,7 @@ Options:
6972
-s, --sourcemap-dirs <dirs> Directories to search for sourcemaps (colon/semicolon-separated)
7073
-n, --node-modules-source-maps <mods> Node modules to load sourcemaps from (comma-separated, e.g., "next,@next/next-server")
7174
--node-options <options> Node.js CLI options to pass to the profiled process
75+
--md-format <format> Markdown format: summary (default), detailed, or adaptive
7276
-h, --help Show this help message
7377
-v, --version Show version number
7478
@@ -80,6 +84,7 @@ Examples:
8084
flame run --delay=until-started server.js # Start profiling after next event loop tick (default)
8185
flame run --sourcemap-dirs=dist:build server.js # Enable sourcemap support
8286
flame run -n next,@next/next-server server.js # Load Next.js sourcemaps from node_modules
87+
flame run --md-format=detailed server.js # Use detailed markdown format
8388
flame run --node-options="--require ts-node/register" server.ts # With Node.js options
8489
flame run --node-options="--import ./loader.js --max-old-space-size=4096" server.js
8590
flame generate profile.pb.gz
@@ -136,12 +141,20 @@ async function main () {
136141
? args['node-modules-source-maps'].split(',').map(s => s.trim())
137142
: undefined
138143

144+
// Parse markdown format option
145+
const mdFormat = args['md-format'] || 'summary'
146+
if (!['summary', 'detailed', 'adaptive'].includes(mdFormat)) {
147+
console.error(`Error: Invalid md-format value '${mdFormat}'. Must be 'summary', 'detailed', or 'adaptive'.`)
148+
process.exit(1)
149+
}
150+
139151
const { pid, process: childProcess } = startProfiling(script, scriptArgs, {
140152
autoStart,
141153
nodeOptions,
142154
delay,
143155
sourcemapDirs,
144-
nodeModulesSourceMaps
156+
nodeModulesSourceMaps,
157+
mdFormat
145158
})
146159

147160
console.log(`🔥 Started profiling process ${pid}`)
@@ -188,13 +201,30 @@ async function main () {
188201
process.exit(1)
189202
}
190203

204+
// Parse markdown format option
205+
const mdFormat = args['md-format'] || 'summary'
206+
if (!['summary', 'detailed', 'adaptive'].includes(mdFormat)) {
207+
console.error(`Error: Invalid md-format value '${mdFormat}'. Must be 'summary', 'detailed', or 'adaptive'.`)
208+
process.exit(1)
209+
}
210+
191211
const outputFile = args.output || `${path.basename(pprofFile, path.extname(pprofFile))}.html`
212+
const mdOutputFile = outputFile.replace(/\.html$/, '.md')
192213
const profileType = path.basename(pprofFile).includes('heap') ? 'Heap' : 'CPU'
193214

194215
console.log(`🔥 Generating ${profileType} flamegraph from ${pprofFile}...`)
195216
await generateFlamegraph(pprofFile, outputFile)
196217
console.log(`🔥 ${profileType} flamegraph generated: ${outputFile}`)
197218
console.log(`🔥 Open file://${path.resolve(outputFile)} in your browser to view the flamegraph`)
219+
220+
// Generate markdown
221+
console.log(`🔥 Generating ${profileType} markdown analysis...`)
222+
try {
223+
await generateMarkdown(pprofFile, mdOutputFile, { format: mdFormat })
224+
console.log(`🔥 ${profileType} markdown generated: ${mdOutputFile}`)
225+
} catch (error) {
226+
console.error(`Warning: Failed to generate ${profileType} markdown:`, error.message)
227+
}
198228
break
199229
}
200230

lib/index.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ function startProfiling (script, args = [], options = {}) {
2424
...process.env,
2525
...options.env,
2626
FLAME_AUTO_START: options.autoStart ? 'true' : 'false',
27-
FLAME_DELAY: options.delay ?? 'until-started'
27+
FLAME_DELAY: options.delay ?? 'until-started',
28+
FLAME_MD_FORMAT: options.mdFormat || 'summary'
2829
}
2930

3031
// Add sourcemap configuration if provided
@@ -142,8 +143,27 @@ async function generateFlamegraph (pprofPath, outputPath) {
142143
})
143144
}
144145

146+
/**
147+
* Generate LLM-friendly markdown analysis from a pprof file
148+
* @param {string} pprofPath - Path to the pprof file
149+
* @param {string} outputPath - Path to write the markdown file
150+
* @param {object} options - Options for markdown generation
151+
* @param {string} options.format - Markdown format: 'summary' (default), 'detailed', or 'adaptive'
152+
* @returns {Promise<object>} Result with outputPath
153+
*/
154+
async function generateMarkdown (pprofPath, outputPath, options = {}) {
155+
const { convert } = await import('pprof-to-md')
156+
const markdown = convert(pprofPath, {
157+
format: options.format || 'summary',
158+
profileName: path.basename(pprofPath)
159+
})
160+
fs.writeFileSync(outputPath, markdown)
161+
return { outputPath }
162+
}
163+
145164
module.exports = {
146165
startProfiling,
147166
parseProfile,
148-
generateFlamegraph
167+
generateFlamegraph,
168+
generateMarkdown
149169
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@datadog/pprof": "^5.9.0",
4141
"fastify": "^5.5.0",
4242
"pprof-format": "^2.2.1",
43+
"pprof-to-md": "^0.1.0",
4344
"react-pprof": "^1.4.0"
4445
},
4546
"devDependencies": {
@@ -59,7 +60,7 @@
5960
"neostandard": "^0.12.0"
6061
},
6162
"engines": {
62-
"node": ">=20.0.0"
63+
"node": ">=22.6.0"
6364
},
6465
"pre-commit": [
6566
"lint",

preload.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ let isCpuProfilerRunning = false
1111
let isHeapProfilerRunning = false
1212
let sourceMapper = null
1313
const autoStart = process.env.FLAME_AUTO_START === 'true'
14+
const mdFormat = process.env.FLAME_MD_FORMAT || 'summary'
1415

1516
// Initialize sourcemap support if enabled
1617
const sourcemapDirs = process.env.FLAME_SOURCEMAP_DIRS
@@ -215,6 +216,16 @@ function generateFlamegraph (pprofPath, outputPath) {
215216
})
216217
}
217218

219+
async function generateMarkdown (pprofPath, outputPath, format = 'summary') {
220+
const { convert } = await import('pprof-to-md')
221+
const markdown = convert(pprofPath, {
222+
format,
223+
profileName: path.basename(pprofPath)
224+
})
225+
fs.writeFileSync(outputPath, markdown)
226+
return { outputPath }
227+
}
228+
218229
function stopProfilerQuick () {
219230
if (!isCpuProfilerRunning && !isHeapProfilerRunning) {
220231
return null
@@ -293,6 +304,16 @@ async function stopProfilerAndSave (generateHtml = false) {
293304
} catch (error) {
294305
console.error('Warning: Failed to generate CPU flamegraph:', error.message)
295306
}
307+
308+
// Generate markdown analysis
309+
const mdFilename = cpuFilename.replace('.pb', '.md')
310+
console.log('🔥 Generating CPU markdown analysis...')
311+
try {
312+
await generateMarkdown(cpuFilename, mdFilename, mdFormat)
313+
console.log(`🔥 CPU markdown generated: ${mdFilename}`)
314+
} catch (error) {
315+
console.error('Warning: Failed to generate CPU markdown:', error.message)
316+
}
296317
}
297318
}
298319

@@ -319,6 +340,16 @@ async function stopProfilerAndSave (generateHtml = false) {
319340
} catch (error) {
320341
console.error('Warning: Failed to generate heap flamegraph:', error.message)
321342
}
343+
344+
// Generate markdown analysis
345+
const mdFilename = heapFilename.replace('.pb', '.md')
346+
console.log('🔥 Generating heap markdown analysis...')
347+
try {
348+
await generateMarkdown(heapFilename, mdFilename, mdFormat)
349+
console.log(`🔥 Heap markdown generated: ${mdFilename}`)
350+
} catch (error) {
351+
console.error('Warning: Failed to generate heap markdown:', error.message)
352+
}
322353
}
323354
}
324355

@@ -350,6 +381,17 @@ function generateHtmlAsync (filenames) {
350381
.catch(error => {
351382
console.error(`Warning: Failed to generate ${profileType} flamegraph:`, error.message)
352383
})
384+
385+
// Generate markdown analysis
386+
const mdFilename = filename.replace('.pb', '.md')
387+
console.log(`🔥 Generating ${profileType} markdown analysis...`)
388+
generateMarkdown(filename, mdFilename, mdFormat)
389+
.then(() => {
390+
console.log(`🔥 ${profileType} markdown generated: ${mdFilename}`)
391+
})
392+
.catch(error => {
393+
console.error(`Warning: Failed to generate ${profileType} markdown:`, error.message)
394+
})
353395
})
354396
}
355397

0 commit comments

Comments
 (0)