Skip to content
Open
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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Options:
-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
--deopt Track V8 deoptimizations and show them on the CPU flamegraph
-h, --help Show help message
-v, --version Show version number
```
Expand All @@ -100,6 +101,9 @@ const { pid, toggleProfiler } = startProfiling('server.js', ['--port', '3000'],
// Or start in manual mode
const { pid, toggleProfiler } = startProfiling('server.js', ['--port', '3000'], { autoStart: false })

// Track V8 deoptimizations while profiling
startProfiling('server.js', [], { autoStart: true, deopt: true })

console.log(`Started profiling process ${pid}`)

// Toggle profiling programmatically (useful in manual mode)
Expand All @@ -123,6 +127,55 @@ const profile = await parseProfile('profile.pb')
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

## V8 Deoptimizations

```bash
flame run --deopt server.js
```

With `--deopt`, flame records the V8 deoptimizations that happen while the
CPU profiler runs and stores them in the CPU profile. The flamegraph then:

- marks frames that had actionable deopts (a bar along the bottom edge and a
triangle in the top-right corner)
- shows the deopt count and the most frequent reasons in the hover tooltip
- lists every deopt of the selected frame in the details panel, with its source
position and an explanation of the reason
- adds a **Highlight Deopts** checkbox to turn the markers off

The markdown report gets a **V8 Deoptimizations** section listing the functions
with actionable deopts, their share of the profile and an explanation of each
reason. Deopts that are part of V8's normal tiering (on-stack replacement,
insufficient type feedback) are counted but not highlighted. A deopt is not a
bug by itself: it matters when it repeats in a function that takes a large
share of the profile.

Explanations come from a catalog of V8's deopt reasons shipped with
`react-pprof`, derived from
[deoptigate's deoptimization guide](https://github.com/Uzlopak/deoptigate/blob/59e854d5cabfe2f9698246c985ad21f3a820b0b8/assets/deoptimization.md).

### How it works

V8 does not reliably write deopt records to its `--log-deopt` log while a CPU
profile is being recorded, so flame reads V8's deopt traces instead: the process starts
with `--trace-deopt-verbose` writing to a temporary file
(`--redirect-code-traces-to`), and the preload turns tracing off right away and
back on only while the profiler runs, including windows started with `SIGUSR2`.
At the end of each window the new traces are parsed, matched to the functions
in the profile, and stored as a JSON comment in the `.pb` file, so
`flame generate` shows them too.

### Notes

- Deopts that happen before profiling starts (for example during startup with
the default `--delay=until-started`) are not recorded; use `--delay=none` to
include them.
- Verbose deopt traces include the deoptimized frames, so programs that
deoptimize a lot write a large temporary file; it is removed when the process
exits.
- Worker threads are traced too, but their deopts are only matched to functions
that appear in the main thread's CPU profile.

## Sourcemap Support

When profiling transpiled or bundled applications (TypeScript, Webpack, ESBuild, etc.), the flame tool can automatically translate stack traces from generated code back to your original source files using sourcemaps.
Expand Down
11 changes: 10 additions & 1 deletion bin/flame.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const { values: args, positionals } = parseArgs({
},
'md-format': {
type: 'string'
},
deopt: {
type: 'boolean'
}
},
allowPositionals: true
Expand Down Expand Up @@ -73,6 +76,7 @@ Options:
-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
--deopt Track V8 deoptimizations while profiling and show them on the flamegraph
-h, --help Show this help message
-v, --version Show version number

Expand All @@ -85,6 +89,7 @@ Examples:
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 --deopt server.js # Highlight V8 deoptimizations on the flamegraph
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 @@ -154,13 +159,17 @@ async function main () {
delay,
sourcemapDirs,
nodeModulesSourceMaps,
mdFormat
mdFormat,
deopt: args.deopt
})

console.log(`🔥 Started profiling process ${pid}`)
if (autoStart) {
console.log('🔥 CPU profiling is active and will generate profile on exit')
console.log('🔥 Profile (.pb) and interactive HTML flamegraph will be auto-generated')
if (args.deopt) {
console.log('🔥 V8 deoptimizations will be highlighted on the CPU flamegraph')
}
console.log('🔥 Generated files will be saved in the current directory')
console.log('Send SIGUSR2 to manually toggle profiling:')
} else {
Expand Down
Loading
Loading