Skip to content

fix(profiler): recover frozen virtual-thread callers - #879

Draft
kaahos wants to merge 2 commits into
paul.fournillon/fix-frozen-virtual-thread-truncationfrom
paul.fournillon/recover-frozen-virtual-thread-callers
Draft

kaahos wants to merge 2 commits into
paul.fournillon/fix-frozen-virtual-thread-truncationfrom
paul.fournillon/recover-frozen-virtual-thread-callers

Conversation

@kaahos

@kaahos kaahos commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?:

Recover older Java callers from HotSpot heap stack chunks when a mounted virtual thread reaches a continuation return barrier. Append interpreted and compiled frames, including inlined callers and parent chunks, to the existing trace buffer before stopping at the continuation root (cstack=vm) or walking the carrier (cstack=vmx).

Extend regression coverage for both walking modes, CPU and wall sampling, parent chunks, depth limits, native calls, unsupported recovery, and platform-thread/fully thawed controls.

Motivation:

HotSpot can thaw only part of a virtual thread's stack after remounting it. The remaining Java callers stay in heap chunks and are absent from a walk of the carrier's native stack. #877 correctly marks that omission as truncated; this change restores those callers where recovery is supported.

Additional Notes:

  • Stacked on fix(profiler): mark skipped frozen virtual-thread callers as truncated #877 (paul.fournillon/fix-frozen-virtual-thread-truncation). Retarget to main after that dependency merges.
  • Recovery is gated to HotSpot JDK 21–27, x86_64/aarch64, G1/Serial/Parallel, available layout metadata, and samples interrupted in an interpreted or compiled Java body. Traversal and output depth are bounded.
  • Native/runtime/VM states, unsupported collectors or layouts, and incomplete recovery retain the truncation fallback. This draft does not restore every omitted caller in every sampling state.
  • Runtime and compilation validation covered arm64. x86_64, JDK 22–24/26, musl, and ASan/TSan were not tested. The complete Java integration suite was not run.
  • A separate order-sensitive carrier test failure remains: on Linux/JDK 27 with Fix continuation metadata lookup and frame alignment #870's metadata fixes applied, the existing carrier check passes alone but fails after the same workload prelude with both the old PR fix(profiler): mark skipped frozen virtual-thread callers as truncated #877 library and the recovery draft. Both failures report break_cont_carrier_sp without encountering a return barrier; this PR does not claim to fix it.

How to test the change?:

./.claude/commands/build-and-summarize spotlessApply :ddprof-test:testDebug \
  -Ptests=FrozenVirtualThreadStackTest -Pskip-gtest -PkeepJFRs --max-workers=2
  • Eight restoration cases fail against the PR fix(profiler): mark skipped frozen virtual-thread callers as truncated #877 native library because outer callers are missing. The final clean macOS/JDK 25 run passes 15 cases with two conditional GC skips, without retries.
  • The selected class also passes on Linux/glibc arm64 JDK 21/27 and macOS arm64 JDK 27, and under interpreter-only, C1-only, uncompressed-oop, Serial, and Parallel configurations. Selected ZGC cases verify the truncated fallback.
  • Native debug suite: 884 passed, six skipped. Independent JDK JFR-consumer checks verify caller count/order, absence of duplicate callers, and completeness metadata.
  • Pre-PR spotlessApply and git diff --check pass; the latest testDebug invocation was up-to-date and adds no new test executions to the results above.

For Datadog employees:

  • If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a security review.
  • This PR doesn't touch any of that.
  • Related JIRA: PROF-16269

Comment on lines +88 to +90
// G1/Serial/Parallel relocate objects at safepoints. A sampled Java thread
// cannot reach a safepoint while its signal handler is reading these oops.
// Concurrently relocating collectors require load barriers not provided here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the profiling signal land on a thread with frozen stack and already in safepoint?
Or is this structurally impossible?

@dd-octo-sts

dd-octo-sts Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #39465168904 | Commit: 23b83cf | Duration: 24m 43s (longest job)

❌ 4 of 32 test jobs failed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - ✅ - -
8-ibm - ✅ - -
8-j9 ✅ ✅ - -
8-librca - - ✅ ✅
8-orcl - ✅ - -
11 - ✅ - -
11-j9 ✅ ✅ - -
11-librca - - ✅ ✅
17 ✅ ✅ - -
17-graal ✅ ✅ - -
17-j9 ✅ ✅ - -
17-librca - - ✅ ✅
21 ❌ ❌ - -
21-graal ✅ ❌ - -
21-librca - - ✅ ✅
25 ❌ ✅ - -
25-graal ✅ ✅ - -
25-librca - - ✅ ✅

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

⚠️ Flaky tests — failed, then passed on retry

Test Cells Message
FrozenVirtualThreadStackTest.cstack_vmx glibc-21-debug-aarch64 org.opentest4j.AssertionFailedError: Application callers were recovered in the wrong order ==> expected: <0> but was: <8
VirtualThreadWallClockTest.cstack_vmx glibc-21-graal-debug-amd64 org.opentest4j.AssertionFailedError: No sample showed carrier-thread frames (ForkJoinWorkerThread) — continuation unwind

These fail the build. Passing on a second run makes a test flaky, not passing. Fix it, or quarantine it against a ticket so the debt is tracked rather than forgotten.

Consider quarantining these — click for ready-made entries

A quarantined test still runs and still reports; its failures just stop
turning CI red. To quarantine one:

  1. Open a PROF ticket for the test, linking the failing job.
  2. Append the line below to ddprof-test/quarantine.txt, replacing
    PROF-XXXXX with the ticket number.
  3. Check the cells and reason columns — the proposal only knows what
    failed in this run, and a narrower cells glob keeps the same test
    gating everywhere it has not misbehaved.

CI fails once review_by passes, so an entry expires instead of piling up.

# test | ticket | added | review_by | cells | reason
# WIDENED: observed `com.datadoghq.profiler.wallclock.FrozenVirtualThreadStackTest.cstack=vmx`, which covers() cannot match exactly; this entry mutes the whole class
com.datadoghq.profiler.wallclock.FrozenVirtualThreadStackTest.* | PROF-XXXXX | 2026-10-20 | 2027-01-18 | glibc-21-debug-aarch64 | org.opentest4j.AssertionFailedError: Application callers were recovered in the wrong order ==> expected: <0> but was: <88> (seen in: glibc-21-debug-aarch64)
# WIDENED: observed `com.datadoghq.profiler.wallclock.VirtualThreadWallClockTest.cstack=vmx`, which covers() cannot match exactly; this entry mutes the whole class
com.datadoghq.profiler.wallclock.VirtualThreadWallClockTest.* | PROF-XXXXX | 2026-10-20 | 2027-01-18 | glibc-21-graal-debug-amd64 | org.opentest4j.AssertionFailedError: No sample showed carrier-thread frames (ForkJoinWorkerThread) — continuation unwind may be broken ==> expected: <true> but was: <false> (seen in: glibc-21-graal-debug-amd64)

❌ Failing tests

Test Cells Message
VirtualThreadWallClockTest.cstack_vmx glibc-21-debug-aarch64, glibc-25-debug-aarch64, glibc-21-debug-amd64 org.opentest4j.AssertionFailedError: No sample showed carrier-thread frames (ForkJoinWorkerThread) — continuation unwind

Retried 4 of 32 cells.

Failed Jobs

Summary: Total: 32 | Passed: 28 | Failed: 4


Updated: 2026-10-20 17:48:35 UTC

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.

2 participants