Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 37 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- **Dual Profiling**: Captures both CPU and heap profiles concurrently for comprehensive performance insights
- **Auto-Start Profiling**: Profiling starts immediately when using `flame run` (default behavior)
- **Automatic Flamegraph Generation**: Interactive HTML flamegraphs are created automatically for both CPU and heap profiles on exit
- **LLM-Friendly Markdown Analysis**: Generates markdown reports with hotspot analysis, ideal for AI-assisted performance debugging
- **Sourcemap Support**: Automatically translates transpiled code locations back to original source files (TypeScript, bundled JavaScript, etc.)
- **Clear File Path Display**: Shows exact paths and browser URLs for generated files
- **Manual Control**: Optional manual mode with signal-based control using `SIGUSR2`
Expand Down Expand Up @@ -34,10 +35,11 @@ flame run server.js
# 🔥 Heap profile written to: heap-profile-2025-08-27T12-00-00-000Z.pb
# 🔥 Generating CPU flamegraph...
# 🔥 CPU flamegraph generated: cpu-profile-2025-08-27T12-00-00-000Z.html
# 🔥 CPU markdown generated: cpu-profile-2025-08-27T12-00-00-000Z.md
# 🔥 Generating heap flamegraph...
# 🔥 Heap flamegraph generated: heap-profile-2025-08-27T12-00-00-000Z.html
# 🔥 Heap markdown generated: heap-profile-2025-08-27T12-00-00-000Z.md
# 🔥 Open file:///path/to/cpu-profile-2025-08-27T12-00-00-000Z.html in your browser to view the CPU flamegraph
# 🔥 Open file:///path/to/heap-profile-2025-08-27T12-00-00-000Z.html in your browser to view the heap flamegraph
```

### Manual Profiling Mode
Expand All @@ -53,14 +55,17 @@ kill -USR2 <PID>
flame toggle
```

### Generate Flamegraph
### Generate Flamegraph and Markdown

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

# Specify custom output file
flame generate -o my-flamegraph.html profile.pb.gz

# Use detailed markdown format for comprehensive analysis
flame generate --md-format=detailed profile.pb
```

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

Commands:
run <script> Run a script with profiling enabled
generate <pprof-file> Generate HTML flamegraph from pprof file
generate <pprof-file> Generate HTML flamegraph and markdown from pprof file
toggle Toggle profiling for running flame processes

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

```javascript
const { startProfiling, generateFlamegraph, parseProfile } = require('@platformatic/flame')
const { startProfiling, generateFlamegraph, generateMarkdown, parseProfile } = require('@platformatic/flame')

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

// Generate LLM-friendly markdown analysis
await generateMarkdown('profile.pb', 'analysis.md', { format: 'summary' })

// Parse profile data
const profile = await parseProfile('profile.pb')
```

## How It Works

1. **Auto-Start Mode (Default)**: Both CPU and heap profiling begin immediately when `flame run` starts your script
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
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
3. **Manual Mode**: Use `--manual` flag to require `SIGUSR2` signals for start/stop control (no auto-HTML generation)
4. **Interactive Visualization**: The `@platformatic/react-pprof` library generates interactive WebGL-based HTML flamegraphs for both profile types
5. **Markdown Analysis**: The `pprof-to-md` library generates LLM-friendly markdown reports with hotspot tables for AI-assisted debugging

## Sourcemap Support

Expand Down Expand Up @@ -167,11 +177,23 @@ flame run server.js

Profile files are saved with timestamps in the format:
```
cpu-profile-2024-01-01T12-00-00-000Z.pb
cpu-profile-2024-01-01T12-00-00-000Z.pb # Binary pprof data
cpu-profile-2024-01-01T12-00-00-000Z.html # Interactive flamegraph
cpu-profile-2024-01-01T12-00-00-000Z.md # LLM-friendly markdown analysis
heap-profile-2024-01-01T12-00-00-000Z.pb
heap-profile-2024-01-01T12-00-00-000Z.html
heap-profile-2024-01-01T12-00-00-000Z.md
```

Both CPU and heap profiles share the same timestamp for easy correlation. The files are compressed Protocol Buffer format compatible with the pprof ecosystem.
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.

### Markdown Formats

The `--md-format` option controls the markdown output:

- **summary** (default): Compact hotspots table, ideal for AI triage and quick overview
- **detailed**: Comprehensive analysis with full stack traces and detailed statistics
- **adaptive**: Automatically chooses format based on profile complexity

## Integration with Existing Apps

Expand Down Expand Up @@ -200,14 +222,15 @@ curl http://localhost:3000
curl http://localhost:3000
curl http://localhost:3000

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

**Manual Mode:**
Expand Down Expand Up @@ -296,9 +319,10 @@ kill -USR2 <PID>

## Requirements

- Node.js >= 18.0.0
- `@datadog/pprof` for CPU profiling
- `@platformatic/react-pprof` for flamegraph generation
- Node.js >= 22.6.0
- `@datadog/pprof` for CPU and heap profiling
- `react-pprof` for flamegraph generation
- `pprof-to-md` for markdown analysis generation

## License

Expand Down
36 changes: 33 additions & 3 deletions bin/flame.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const { parseArgs } = require('node:util')
const fs = require('fs')
const path = require('path')
const { startProfiling, generateFlamegraph } = require('../lib/index.js')
const { startProfiling, generateFlamegraph, generateMarkdown } = require('../lib/index.js')

const { values: args, positionals } = parseArgs({
args: process.argv.slice(2),
Expand Down Expand Up @@ -42,6 +42,9 @@ const { values: args, positionals } = parseArgs({
'node-modules-source-maps': {
type: 'string',
short: 'n'
},
'md-format': {
type: 'string'
}
},
allowPositionals: true
Expand All @@ -59,7 +62,7 @@ Usage: flame [options] <command>

Commands:
run <script> Run a script with profiling enabled
generate <pprof-file> Generate HTML flamegraph from pprof file
generate <pprof-file> Generate HTML flamegraph and markdown from pprof file

Options:
-o, --output <file> Output HTML file (for generate command)
Expand All @@ -69,6 +72,7 @@ Options:
-s, --sourcemap-dirs <dirs> Directories to search for sourcemaps (colon/semicolon-separated)
-n, --node-modules-source-maps <mods> Node modules to load sourcemaps from (comma-separated, e.g., "next,@next/next-server")
--node-options <options> Node.js CLI options to pass to the profiled process
--md-format <format> Markdown format: summary (default), detailed, or adaptive
-h, --help Show this help message
-v, --version Show version number

Expand All @@ -80,6 +84,7 @@ Examples:
flame run --delay=until-started server.js # Start profiling after next event loop tick (default)
flame run --sourcemap-dirs=dist:build server.js # Enable sourcemap support
flame run -n next,@next/next-server server.js # Load Next.js sourcemaps from node_modules
flame run --md-format=detailed server.js # Use detailed markdown format
flame run --node-options="--require ts-node/register" server.ts # With Node.js options
flame run --node-options="--import ./loader.js --max-old-space-size=4096" server.js
flame generate profile.pb.gz
Expand Down Expand Up @@ -136,12 +141,20 @@ async function main () {
? args['node-modules-source-maps'].split(',').map(s => s.trim())
: undefined

// Parse markdown format option
const mdFormat = args['md-format'] || 'summary'
if (!['summary', 'detailed', 'adaptive'].includes(mdFormat)) {
console.error(`Error: Invalid md-format value '${mdFormat}'. Must be 'summary', 'detailed', or 'adaptive'.`)
process.exit(1)
}

const { pid, process: childProcess } = startProfiling(script, scriptArgs, {
autoStart,
nodeOptions,
delay,
sourcemapDirs,
nodeModulesSourceMaps
nodeModulesSourceMaps,
mdFormat
})

console.log(`🔥 Started profiling process ${pid}`)
Expand Down Expand Up @@ -188,13 +201,30 @@ async function main () {
process.exit(1)
}

// Parse markdown format option
const mdFormat = args['md-format'] || 'summary'
if (!['summary', 'detailed', 'adaptive'].includes(mdFormat)) {
console.error(`Error: Invalid md-format value '${mdFormat}'. Must be 'summary', 'detailed', or 'adaptive'.`)
process.exit(1)
}

const outputFile = args.output || `${path.basename(pprofFile, path.extname(pprofFile))}.html`
const mdOutputFile = outputFile.replace(/\.html$/, '.md')
const profileType = path.basename(pprofFile).includes('heap') ? 'Heap' : 'CPU'

console.log(`🔥 Generating ${profileType} flamegraph from ${pprofFile}...`)
await generateFlamegraph(pprofFile, outputFile)
console.log(`🔥 ${profileType} flamegraph generated: ${outputFile}`)
console.log(`🔥 Open file://${path.resolve(outputFile)} in your browser to view the flamegraph`)

// Generate markdown
console.log(`🔥 Generating ${profileType} markdown analysis...`)
try {
await generateMarkdown(pprofFile, mdOutputFile, { format: mdFormat })
console.log(`🔥 ${profileType} markdown generated: ${mdOutputFile}`)
} catch (error) {
console.error(`Warning: Failed to generate ${profileType} markdown:`, error.message)
}
break
}

Expand Down
24 changes: 22 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ function startProfiling (script, args = [], options = {}) {
...process.env,
...options.env,
FLAME_AUTO_START: options.autoStart ? 'true' : 'false',
FLAME_DELAY: options.delay ?? 'until-started'
FLAME_DELAY: options.delay ?? 'until-started',
FLAME_MD_FORMAT: options.mdFormat || 'summary'
}

// Add sourcemap configuration if provided
Expand Down Expand Up @@ -142,8 +143,27 @@ async function generateFlamegraph (pprofPath, outputPath) {
})
}

/**
* Generate LLM-friendly markdown analysis from a pprof file
* @param {string} pprofPath - Path to the pprof file
* @param {string} outputPath - Path to write the markdown file
* @param {object} options - Options for markdown generation
* @param {string} options.format - Markdown format: 'summary' (default), 'detailed', or 'adaptive'
* @returns {Promise<object>} Result with outputPath
*/
async function generateMarkdown (pprofPath, outputPath, options = {}) {
const { convert } = await import('pprof-to-md')
const markdown = convert(pprofPath, {
format: options.format || 'summary',
profileName: path.basename(pprofPath)
})
fs.writeFileSync(outputPath, markdown)
return { outputPath }
}

module.exports = {
startProfiling,
parseProfile,
generateFlamegraph
generateFlamegraph,
generateMarkdown
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@datadog/pprof": "^5.9.0",
"fastify": "^5.5.0",
"pprof-format": "^2.2.1",
"pprof-to-md": "^0.1.0",
"react-pprof": "^1.4.0"
},
"devDependencies": {
Expand All @@ -59,7 +60,7 @@
"neostandard": "^0.12.0"
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.6.0"
},
"pre-commit": [
"lint",
Expand Down
42 changes: 42 additions & 0 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ let isCpuProfilerRunning = false
let isHeapProfilerRunning = false
let sourceMapper = null
const autoStart = process.env.FLAME_AUTO_START === 'true'
const mdFormat = process.env.FLAME_MD_FORMAT || 'summary'

// Initialize sourcemap support if enabled
const sourcemapDirs = process.env.FLAME_SOURCEMAP_DIRS
Expand Down Expand Up @@ -215,6 +216,16 @@ function generateFlamegraph (pprofPath, outputPath) {
})
}

async function generateMarkdown (pprofPath, outputPath, format = 'summary') {
const { convert } = await import('pprof-to-md')
const markdown = convert(pprofPath, {
format,
profileName: path.basename(pprofPath)
})
fs.writeFileSync(outputPath, markdown)
return { outputPath }
}

function stopProfilerQuick () {
if (!isCpuProfilerRunning && !isHeapProfilerRunning) {
return null
Expand Down Expand Up @@ -293,6 +304,16 @@ async function stopProfilerAndSave (generateHtml = false) {
} catch (error) {
console.error('Warning: Failed to generate CPU flamegraph:', error.message)
}

// Generate markdown analysis
const mdFilename = cpuFilename.replace('.pb', '.md')
console.log('🔥 Generating CPU markdown analysis...')
try {
await generateMarkdown(cpuFilename, mdFilename, mdFormat)
console.log(`🔥 CPU markdown generated: ${mdFilename}`)
} catch (error) {
console.error('Warning: Failed to generate CPU markdown:', error.message)
}
}
}

Expand All @@ -319,6 +340,16 @@ async function stopProfilerAndSave (generateHtml = false) {
} catch (error) {
console.error('Warning: Failed to generate heap flamegraph:', error.message)
}

// Generate markdown analysis
const mdFilename = heapFilename.replace('.pb', '.md')
console.log('🔥 Generating heap markdown analysis...')
try {
await generateMarkdown(heapFilename, mdFilename, mdFormat)
console.log(`🔥 Heap markdown generated: ${mdFilename}`)
} catch (error) {
console.error('Warning: Failed to generate heap markdown:', error.message)
}
}
}

Expand Down Expand Up @@ -350,6 +381,17 @@ function generateHtmlAsync (filenames) {
.catch(error => {
console.error(`Warning: Failed to generate ${profileType} flamegraph:`, error.message)
})

// Generate markdown analysis
const mdFilename = filename.replace('.pb', '.md')
console.log(`🔥 Generating ${profileType} markdown analysis...`)
generateMarkdown(filename, mdFilename, mdFormat)
.then(() => {
console.log(`🔥 ${profileType} markdown generated: ${mdFilename}`)
})
.catch(error => {
console.error(`Warning: Failed to generate ${profileType} markdown:`, error.message)
})
})
}

Expand Down
Loading
Loading