Skip to content

feat: add --deopt to show V8 deoptimizations on the flamegraph - #149

Open
mcollina wants to merge 1 commit into
mainfrom
feat/deopt-tracking
Open

mcollina wants to merge 1 commit into
mainfrom
feat/deopt-tracking

Conversation

@mcollina

@mcollina mcollina commented Nov 3, 2026

Copy link
Copy Markdown
Member

Adds flame run --deopt. It records the V8 deoptimizations that happen while the CPU profiler runs and stores them in the CPU profile. The flamegraph shows them (markers, tooltip, details panel) and the markdown report gets a new section.

Depends on platformatic/react-pprof#54. That PR does the rendering and provides the reason catalog. This PR bumps react-pprof to ^1.6.0, assuming that's the release that ships it, and doesn't update package-lock.json because that version isn't published yet. CI will fail until it is; after the release, run npm install to update the lockfile.

How it works

I first tried V8's log (--log-deopt, toggled at runtime). That works without a profiler, but V8 does not reliably write code-deopt records while a CPU profile is being recorded. With @datadog/pprof running, a test with 4 deopts logged 1; with the inspector profiler it logged 0. The [bailout …] traces are still emitted, so this PR reads those instead:

  1. startProfiling({ deopt: true }) starts the process with --trace-deopt-verbose --redirect-code-traces --redirect-code-traces-to=<tmpdir>/deopt-trace.txt.
    • The flag has to be on at startup, because that's when V8 opens the trace file. Turning it on later prints nothing.
    • Only the verbose mode prints the deopt's source position (;;; deoptimize at <file:line:col>).
    • The traces never reach the app's stdout.
  2. preload.js turns tracing off immediately. It turns it back on when the CPU profiler starts and off when it stops, so SIGUSR2 windows work too. Toggling works on Node 22, 24 and 26.
  3. When profiling stops, lib/deopt.js parses the part of the trace file written during the window. For each deopt it takes the reason, the bailout kind, the function name and the position. It then assigns the deopt to a function in the profile using the outermost (non-inlined) position and the function name, picking the closest function start at or before that position.
    • @datadog/pprof leaves Function.startLine empty and stores the start line in the location, so the matcher uses that. It's the same line react-pprof uses for frames, which makes the match exact.
  4. The result is stored as a v8-deopts JSON comment in the .pb, so flame generate shows the deopts too. The temporary directory is deleted when the child process exits.

Markdown

When a CPU profile has deopt data, generateMarkdown (and the preload's copy of it) adds a V8 Deoptimizations section:

  • a table of the functions with actionable deopts, with their self/total share of the profile and the reasons
  • a list of each deopt with its position and the explanation from the catalog

Tiering events (OSR, insufficient type feedback) are counted, but their functions aren't listed.

Limitations

  • Deopts that happen before profiling starts aren't recorded. With the default --delay=until-started, that includes the first synchronous run of the script; --delay=none captures it.
  • The verbose trace includes frame translation details, so programs that deoptimize a lot write a large temporary file.
  • Worker threads share the trace file. Their deopts are only matched to functions that appear in the main thread's profile.

Tests

  • New test/deopt.test.js:
    • unit tests for trace parsing, inlined positions, function matching, the payload surviving encode/decode, and the markdown output
    • an end-to-end flame run --deopt --delay=none test, which checks that deopts are attached to the script's functions, that no [bailout lines reach stdout, and that the markdown section is written
  • Full suite: 99 pass, 1 skipped. The new tests also pass on Node 22.22.2.
  • These ran against a local build of react-pprof#54 symlinked into node_modules. With the published react-pprof@1.5.1, the markdown tests fail because the catalog isn't there.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sfj4nDTRSqdZpTPUFfHCyZ

`flame run --deopt` records the V8 deoptimizations that happen while the
CPU profiler runs and stores them in the CPU profile as a `v8-deopts`
JSON comment, which react-pprof renders as markers, tooltip entries and
a details panel. The markdown report gets a "V8 Deoptimizations"
section with the functions that have actionable deopts, their share of
the profile and an explanation of each reason.

V8 does not reliably write `code-deopt` records to its log while a CPU
profile is being recorded, so deopts are read from V8's deopt traces:
the process starts with --trace-deopt-verbose redirected to a temporary
file, and the preload turns tracing off at startup and back on only
during profiling windows (including SIGUSR2 toggles).

Requires react-pprof with deopt support (^1.6.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfj4nDTRSqdZpTPUFfHCyZ

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant