Skip to content

Commit 8ed7897

Browse files
jbachorikclaude
andcommitted
fix(walkvm): unwind through virtual thread continuation boundaries
Fixes walkVM to correctly traverse JVM virtual thread (Project Loom) continuation boundaries, exposing carrier thread frames in wall-clock profiles. Two unwind paths are implemented: - Path A (enterSpecial): CPU-bound VTs that never yield — all frames are thawed; the profiler traverses the enterSpecial nmethod by identity to reach carrier frames via ContinuationEntry. - Path B (cont_returnBarrier): blocking VTs that park/unpark — when remounted with frozen frames in the StackChunk, cont_returnBarrier is the return PC of the bottommost thawed frame. Checked before CodeHeap::findNMethod() since it is a JVM stub, not an nmethod. By default a synthetic "JVM Continuation" root frame (BCI_NATIVE_FRAME) is inserted at the boundary so the sample is not marked truncated. With wextend=vt_carrier the profiler walks through to carrier frames; failures emit BCI_ERROR (truthful truncation). The wextend argument is string-parsed and extensible for future flags. Additional changes: - Add carrier_frames bit to StackWalkFeatures (uses one padding bit) - Use FRAME_PC_SLOT for architecture-portable carrier frame extraction - Split VMContinuationEntry into DECLARE_V21_TYPES_DO to prevent assert(type_size() > 0) on JDK <21 debug builds; expand at all four declare/init/read/verify sites - Three new counters: WALKVM_CONT_BARRIER_HIT, WALKVM_ENTER_SPECIAL_HIT, WALKVM_CONT_ENTRY_NULL - isValidFP / isValidSP helpers with unit tests in stackWalker_ut.cpp - DDPROF_DISABLE_CONT_UNWIND env var for negative testing (DEBUG only) - Integration tests: VirtualThreadWallClockTest covers both paths on JDK 21+ with wextend=vt_carrier Resolves SCP-1110 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5ed7a66 commit 8ed7897

10 files changed

Lines changed: 508 additions & 42 deletions

File tree

ddprof-lib/src/main/cpp/arguments.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,19 @@ Error Arguments::parse(const char *args) {
300300
}
301301
}
302302

303+
CASE("wextend")
304+
if (value != NULL) {
305+
const char* p = value;
306+
while (*p != '\0') {
307+
const char* comma = strchr(p, ',');
308+
size_t len = comma != NULL ? (size_t)(comma - p) : strlen(p);
309+
if (len == 10 && strncmp(p, "vt_carrier", 10) == 0) {
310+
_features.carrier_frames = 1;
311+
}
312+
p += len + (comma != NULL ? 1 : 0);
313+
}
314+
}
315+
303316
CASE("attributes")
304317
if (value != NULL) {
305318
std::string input(value);

ddprof-lib/src/main/cpp/arguments.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ struct StackWalkFeatures {
122122
unsigned short vtable_target : 1; // show receiver classes of vtable/itable stubs
123123
unsigned short comp_task : 1; // display current compilation task for JIT threads
124124
unsigned short pc_addr : 1; // record exact PC address for each sample
125-
unsigned short _padding : 3; // pad structure to 16 bits
125+
unsigned short carrier_frames: 1; // walk through VT continuation boundary to carrier frames (wextend=vt_carrier)
126+
unsigned short _padding : 2; // pad structure to 16 bits
126127
};
127128

128129
struct Multiplier {

ddprof-lib/src/main/cpp/counters.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@
9292
X(WALKVM_STUB_FRAMESIZE_FALLBACK, "walkvm_stub_framesize_fallback") \
9393
X(WALKVM_FP_CHAIN_ATTEMPT, "walkvm_fp_chain_attempt") \
9494
X(WALKVM_FP_CHAIN_REACHED_CODEHEAP, "walkvm_fp_chain_reached_codeheap") \
95-
X(WALKVM_ANCHOR_NOT_IN_JAVA, "walkvm_anchor_not_in_java") \
95+
X(WALKVM_ANCHOR_NOT_IN_JAVA, "walkvm_anchor_not_in_java") \
96+
X(WALKVM_CONT_BARRIER_HIT, "walkvm_cont_barrier_hit") \
97+
X(WALKVM_ENTER_SPECIAL_HIT, "walkvm_enter_special_hit") \
98+
X(WALKVM_CONT_ENTRY_NULL, "walkvm_cont_entry_null") \
9699
X(NATIVE_LIBS_DROPPED, "native_libs_dropped") \
97100
X(SIGACTION_PATCHED_LIBS, "sigaction_patched_libs") \
98101
X(SIGACTION_INTERCEPTED, "sigaction_intercepted")

ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ int VMStructs::_narrow_klass_shift = -1;
3535
int VMStructs::_interpreter_frame_bcp_offset = 0;
3636
unsigned char VMStructs::_unsigned5_base = 0;
3737
const void* VMStructs::_call_stub_return = nullptr;
38+
const void* VMStructs::_cont_return_barrier = nullptr;
39+
const void* VMStructs::_cont_entry_return_pc = nullptr;
40+
VMNMethod* VMStructs::_enter_special_nm = nullptr;
3841
const void* VMStructs::_interpreter_start = nullptr;
3942
VMNMethod* VMStructs::_interpreter_nm = nullptr;
4043
const void* VMStructs::_interpreted_frame_valid_start = nullptr;
@@ -44,6 +47,7 @@ const void* VMStructs::_interpreted_frame_valid_end = nullptr;
4447
// Initialize type size to 0
4548
#define INIT_TYPE_SIZE(name, names) uint64_t VMStructs::TYPE_SIZE_NAME(name) = 0;
4649
DECLARE_TYPES_DO(INIT_TYPE_SIZE)
50+
DECLARE_V21_TYPES_DO(INIT_TYPE_SIZE)
4751
#undef INIT_TYPE_SIZE
4852

4953
#define offset_value -1
@@ -205,6 +209,7 @@ void VMStructs::init_type_sizes() {
205209
}
206210

207211
DECLARE_TYPES_DO(READ_TYPE_SIZE)
212+
DECLARE_V21_TYPES_DO(READ_TYPE_SIZE)
208213

209214
#undef READ_TYPE_SIZE
210215

@@ -273,6 +278,9 @@ void VMStructs::verify_offsets() {
273278
// Verify type sizes
274279
#define VERIFY_TYPE_SIZE(name, names) assert(TYPE_SIZE_NAME(name) > 0);
275280
DECLARE_TYPES_DO(VERIFY_TYPE_SIZE);
281+
if (hotspot_version >= 21) {
282+
DECLARE_V21_TYPES_DO(VERIFY_TYPE_SIZE);
283+
}
276284
#undef VERIFY_TYPE_SIZE
277285

278286

@@ -391,6 +399,12 @@ void VMStructs::resolveOffsets() {
391399
if (_call_stub_return_addr != NULL) {
392400
_call_stub_return = *(const void**)_call_stub_return_addr;
393401
}
402+
if (_cont_return_barrier_addr != NULL) {
403+
_cont_return_barrier = *(const void**)_cont_return_barrier_addr;
404+
}
405+
if (_cont_entry_return_pc_addr != NULL) {
406+
_cont_entry_return_pc = *(const void**)_cont_entry_return_pc_addr;
407+
}
394408

395409
// Since JDK 23, _metadata_offset is relative to _data_offset. See metadata()
396410
if (_nmethod_immutable_offset < 0) {
@@ -440,6 +454,14 @@ void VMStructs::resolveOffsets() {
440454
if (_interpreter_nm == NULL && _interpreter_start != NULL) {
441455
_interpreter_nm = CodeHeap::findNMethod(_interpreter_start);
442456
}
457+
if (_enter_special_nm == NULL && _cont_entry_return_pc != NULL) {
458+
// enterSpecial is a generated nmethod. If findNMethod returns NULL the
459+
// CodeHeap may not yet contain it at VM-ready time; Path A (enterSpecial
460+
// detection) will silently not trigger for those samples, which is safe —
461+
// the stack will simply be truncated at the continuation boundary rather
462+
// than showing carrier frames.
463+
_enter_special_nm = CodeHeap::findNMethod(_cont_entry_return_pc);
464+
}
443465
}
444466

445467
void VMStructs::initJvmFunctions() {

ddprof-lib/src/main/cpp/hotspot/vmStructs.h

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,20 @@ inline T* cast_to(const void* ptr) {
112112
*/
113113

114114
#define DECLARE_TYPES_DO(f) \
115-
f(VMClassLoaderData, MATCH_SYMBOLS("ClassLoaderData")) \
116-
f(VMConstantPool, MATCH_SYMBOLS("ConstantPool")) \
117-
f(VMConstMethod, MATCH_SYMBOLS("ConstMethod")) \
118-
f(VMFlag, MATCH_SYMBOLS("JVMFlag", "Flag")) \
119-
f(VMJavaFrameAnchor, MATCH_SYMBOLS("JavaFrameAnchor")) \
120-
f(VMKlass, MATCH_SYMBOLS("Klass")) \
121-
f(VMMethod, MATCH_SYMBOLS("Method")) \
122-
f(VMNMethod, MATCH_SYMBOLS("nmethod")) \
123-
f(VMSymbol, MATCH_SYMBOLS("Symbol")) \
124-
f(VMThread, MATCH_SYMBOLS("Thread"))
115+
f(VMClassLoaderData, MATCH_SYMBOLS("ClassLoaderData")) \
116+
f(VMConstantPool, MATCH_SYMBOLS("ConstantPool")) \
117+
f(VMConstMethod, MATCH_SYMBOLS("ConstMethod")) \
118+
f(VMFlag, MATCH_SYMBOLS("JVMFlag", "Flag")) \
119+
f(VMJavaFrameAnchor, MATCH_SYMBOLS("JavaFrameAnchor")) \
120+
f(VMKlass, MATCH_SYMBOLS("Klass")) \
121+
f(VMMethod, MATCH_SYMBOLS("Method")) \
122+
f(VMNMethod, MATCH_SYMBOLS("nmethod")) \
123+
f(VMSymbol, MATCH_SYMBOLS("Symbol")) \
124+
f(VMThread, MATCH_SYMBOLS("Thread"))
125+
126+
// Types only present in JDK 21+ (Project Loom); size is 0 on older JDKs
127+
#define DECLARE_V21_TYPES_DO(f) \
128+
f(VMContinuationEntry, MATCH_SYMBOLS("ContinuationEntry"))
125129

126130
/**
127131
* Following macros define field offsets, addresses or values of JVM classes that are exported by
@@ -190,6 +194,7 @@ typedef void* address;
190194
field(_thread_anchor_offset, offset, MATCH_SYMBOLS("_anchor")) \
191195
field(_thread_state_offset, offset, MATCH_SYMBOLS("_thread_state")) \
192196
field(_thread_vframe_offset, offset, MATCH_SYMBOLS("_vframe_array_head")) \
197+
field_with_version(_cont_entry_offset, offset, 21, MAX_VERSION, MATCH_SYMBOLS("_cont_entry")) \
193198
type_end() \
194199
type_begin(VMOSThread, MATCH_SYMBOLS("OSThread")) \
195200
field(_osthread_id_offset, offset, MATCH_SYMBOLS("_thread_id")) \
@@ -246,7 +251,12 @@ typedef void* address;
246251
field(_vs_high_offset, offset, MATCH_SYMBOLS("_high")) \
247252
type_end() \
248253
type_begin(VMStubRoutine, MATCH_SYMBOLS("StubRoutines")) \
249-
field(_call_stub_return_addr, address, MATCH_SYMBOLS("_call_stub_return_address")) \
254+
field(_call_stub_return_addr, address, MATCH_SYMBOLS("_call_stub_return_address")) \
255+
field_with_version(_cont_return_barrier_addr, address, 21, MAX_VERSION, MATCH_SYMBOLS("_cont_returnBarrier")) \
256+
type_end() \
257+
type_begin(VMContinuationEntry, MATCH_SYMBOLS("ContinuationEntry")) \
258+
field_with_version(_cont_entry_return_pc_addr, address, 21, MAX_VERSION, MATCH_SYMBOLS("_return_pc")) \
259+
field_with_version(_cont_entry_parent_offset, offset, 21, MAX_VERSION, MATCH_SYMBOLS("_parent")) \
250260
type_end() \
251261
type_begin(VMGrowableArray, MATCH_SYMBOLS("GrowableArrayBase", "GenericGrowableArray")) \
252262
field(_array_len_offset, offset, MATCH_SYMBOLS("_len")) \
@@ -309,6 +319,9 @@ class VMStructs {
309319
static int _interpreter_frame_bcp_offset;
310320
static unsigned char _unsigned5_base;
311321
static const void* _call_stub_return;
322+
static const void* _cont_return_barrier;
323+
static const void* _cont_entry_return_pc;
324+
static VMNMethod* _enter_special_nm;
312325
static const void* _interpreter_start;
313326
static VMNMethod* _interpreter_nm;
314327
static const void* _interpreted_frame_valid_start;
@@ -320,6 +333,7 @@ class VMStructs {
320333
static uint64_t TYPE_SIZE_NAME(name);
321334

322335
DECLARE_TYPES_DO(DECLARE_TYPE_SIZE_VAR)
336+
DECLARE_V21_TYPES_DO(DECLARE_TYPE_SIZE_VAR)
323337
#undef DECLARE_TYPE_SIZE_VAR
324338

325339
// Declare vmStructs' field offsets and addresses
@@ -444,6 +458,14 @@ class VMStructs {
444458
return pc >= _interpreted_frame_valid_start && pc < _interpreted_frame_valid_end;
445459
}
446460

461+
static bool isContReturnBarrier(const void* pc) {
462+
return _cont_return_barrier != nullptr && pc == _cont_return_barrier;
463+
}
464+
465+
static VMNMethod* enterSpecialNMethod() {
466+
return _enter_special_nm;
467+
}
468+
447469
// Datadog-specific extensions
448470
static bool isSafeToWalk(uintptr_t pc);
449471
static void JNICALL NativeMethodBind(jvmtiEnv *jvmti, JNIEnv *jni,
@@ -671,6 +693,30 @@ DECLARE(VMJavaFrameAnchor)
671693
}
672694
DECLARE_END
673695

696+
DECLARE(VMContinuationEntry)
697+
public:
698+
// Address of the enterSpecial frame's {saved_fp, return_addr} pair.
699+
// Layout above this address: [saved_fp][return_addr_to_carrier][carrier_sp...]
700+
// The ContinuationEntry struct is embedded on the carrier stack immediately
701+
// below enterSpecial's saved-fp slot; its size() equals the JVM's
702+
// ContinuationEntry::size() static method, confirmed at:
703+
// https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/continuationEntry.hpp
704+
// https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/continuationEntry.cpp
705+
uintptr_t entryFP() const {
706+
assert(type_size() > 0); // must not be called before ContinuationEntry is resolved
707+
return (uintptr_t)this + type_size();
708+
}
709+
710+
// Returns the enclosing ContinuationEntry when continuations are nested
711+
// (e.g. a Continuation.run() call inside a virtual thread). Returns
712+
// nullptr when there is no enclosing entry or the field is unavailable.
713+
VMContinuationEntry* parent() const {
714+
if (_cont_entry_parent_offset < 0) return nullptr;
715+
void* ptr = SafeAccess::loadPtr((void**) at(_cont_entry_parent_offset), nullptr);
716+
return ptr != nullptr ? VMContinuationEntry::cast(ptr) : nullptr;
717+
}
718+
DECLARE_END
719+
674720
// Copied from JDK's globalDefinitions.hpp 'JavaThreadState' enum
675721
enum JVMJavaThreadState {
676722
_thread_uninitialized = 0, // should never happen (missing initialization)
@@ -790,6 +836,12 @@ DECLARE(VMThread)
790836
return VMJavaFrameAnchor::cast(at(_thread_anchor_offset));
791837
}
792838

839+
VMContinuationEntry* contEntry() {
840+
if (_cont_entry_offset < 0) return nullptr;
841+
void* ptr = SafeAccess::loadPtr((void**) at(_cont_entry_offset), nullptr);
842+
return ptr != nullptr ? VMContinuationEntry::cast(ptr) : nullptr;
843+
}
844+
793845
inline VMMethod* compiledMethod();
794846
private:
795847
static inline int nativeThreadId(JNIEnv* jni, jthread thread);

ddprof-lib/src/main/cpp/stackWalker.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
#include <cstdlib>
78
#include <setjmp.h>
89
#include "stackWalker.h"
910
#include "dwarf.h"
@@ -19,6 +20,16 @@
1920
const uintptr_t MAX_WALK_SIZE = 0x100000;
2021
const intptr_t MAX_FRAME_SIZE_WORDS = StackWalkValidation::MAX_FRAME_SIZE / sizeof(void*); // 0x8000 = 32768 words
2122

23+
#ifdef NDEBUG
24+
static const bool CONT_UNWIND_DISABLED = false;
25+
#else
26+
// DEBUG-only: when set, both continuation-unwind paths (Path A: enterSpecial,
27+
// Path B: cont_returnBarrier) are skipped, reproducing pre-fix behaviour.
28+
// Used by negative integration tests to verify that carrier frames are not
29+
// visible and walk-error sentinels do appear without the fix.
30+
static const bool CONT_UNWIND_DISABLED = (std::getenv("DDPROF_DISABLE_CONT_UNWIND") != nullptr);
31+
#endif
32+
2233
static ucontext_t empty_ucontext{};
2334

2435
// Use validation helpers from header (shared with tests)
@@ -323,6 +334,9 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext,
323334

324335
const void* prev_native_pc = NULL;
325336

337+
// Last ContinuationEntry crossed; advanced via parent() for nested continuations.
338+
VMContinuationEntry* cont_entry = nullptr;
339+
326340
// Saved anchor data — preserved across anchor consumption so inline
327341
// recovery can redirect even after the anchor pointer has been set to NULL.
328342
// Recovery is one-shot: once attempted, we do not retry to avoid
@@ -339,6 +353,51 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext,
339353
anchor = vm_thread->anchor();
340354
}
341355

356+
static const char* CONT_ROOT_FRAME = "JVM Continuation";
357+
358+
// Advances through a continuation boundary to the carrier frame.
359+
// Without wextend=vt_carrier (default): always stops with a "JVM Continuation"
360+
// synthetic root frame — VT frames are complete, carrier internals are noise.
361+
// With wextend=carrier: attempts to walk through; failures emit BCI_ERROR
362+
// so the sample is truthfully marked truncated.
363+
// Walks cont_entry->parent() on repeated calls to handle nested continuations
364+
// (_parent not triggered by standard single-level VTs today, but required
365+
// once any runtime layers continuations on top of VTs).
366+
// Returns true to continue the walk, false to break.
367+
auto walkThroughContinuation = [&]() -> bool {
368+
if (!features.carrier_frames) {
369+
fillFrame(frames[depth++], BCI_NATIVE_FRAME, CONT_ROOT_FRAME);
370+
return false;
371+
}
372+
cont_entry = (cont_entry != nullptr) ? cont_entry->parent() : vm_thread->contEntry();
373+
if (cont_entry == nullptr) {
374+
Counters::increment(WALKVM_CONT_ENTRY_NULL);
375+
fillFrame(frames[depth++], BCI_ERROR, "break_cont_entry_null");
376+
return false;
377+
}
378+
uintptr_t entry_fp = cont_entry->entryFP();
379+
if (!StackWalkValidation::isValidFP(entry_fp)) {
380+
fillFrame(frames[depth++], BCI_ERROR, "break_cont_entry_fp");
381+
return false;
382+
}
383+
// entry_fp has been range-checked by isValidFP above; any remaining
384+
// SIGSEGV from a stale/concurrently-freed pointer is caught by the
385+
// setjmp crash protection in walkVM (checkFault -> longjmp).
386+
uintptr_t carrier_fp = *(uintptr_t*)entry_fp;
387+
const void* carrier_pc = ((const void**)entry_fp)[FRAME_PC_SLOT];
388+
uintptr_t carrier_sp = entry_fp + (FRAME_PC_SLOT + 1) * sizeof(void*);
389+
if (!StackWalkValidation::isValidFP(carrier_fp) ||
390+
StackWalkValidation::inDeadZone(carrier_pc) ||
391+
!StackWalkValidation::isValidSP(carrier_sp, sp, bottom)) {
392+
fillFrame(frames[depth++], BCI_ERROR, "break_cont_carrier_sp");
393+
return false;
394+
}
395+
sp = carrier_sp;
396+
fp = carrier_fp;
397+
pc = carrier_pc;
398+
return true;
399+
};
400+
342401
unwind_loop:
343402

344403
// Walk until the bottom of the stack or until the first Java frame
@@ -369,6 +428,15 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext,
369428
break;
370429
}
371430
prev_native_pc = NULL; // we are in JVM code, no previous 'native' PC
431+
432+
// cont_returnBarrier is a JVM stub — CodeHeap::findNMethod() returns NULL for it,
433+
// so it must be handled before the nmethod dispatch below.
434+
if (!CONT_UNWIND_DISABLED && VMStructs::isContReturnBarrier(pc)) {
435+
Counters::increment(WALKVM_CONT_BARRIER_HIT);
436+
if (walkThroughContinuation()) continue;
437+
break;
438+
}
439+
372440
VMNMethod* nm = CodeHeap::findNMethod(pc);
373441
if (nm == NULL) {
374442
if (anchor == NULL) {
@@ -444,6 +512,15 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext,
444512
fillFrame(frames[depth++], BCI_ERROR, "break_interpreted");
445513
break;
446514
} else if (nm->isNMethod()) {
515+
// enterSpecial is a generated native nmethod that acts as the
516+
// continuation entry stub. It has no JavaCallWrapper, so
517+
// isEntryFrame() will not fire for it. Detect it by identity
518+
// and navigate to the carrier thread via ContinuationEntry.
519+
if (!CONT_UNWIND_DISABLED && nm == VMStructs::enterSpecialNMethod()) {
520+
Counters::increment(WALKVM_ENTER_SPECIAL_HIT);
521+
if (walkThroughContinuation()) continue;
522+
break;
523+
}
447524
// Check if deoptimization is in progress before walking compiled frames
448525
if (vm_thread != NULL && vm_thread->inDeopt()) {
449526
fillFrame(frames[depth++], BCI_ERROR, "break_deopt_compiled");

ddprof-lib/src/main/cpp/stackWalker.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ namespace StackWalkValidation {
5252
return (uintptr_t)hi - (uintptr_t)lo < SAME_STACK_DISTANCE;
5353
}
5454

55+
// Check if a frame pointer is plausibly valid (not in dead zone, properly aligned)
56+
static inline bool isValidFP(uintptr_t fp) {
57+
return !inDeadZone((const void*)fp) && aligned(fp);
58+
}
59+
60+
// Check if a stack pointer is within [lo, hi) and properly aligned
61+
static inline bool isValidSP(uintptr_t sp, uintptr_t lo, uintptr_t hi) {
62+
return sp > lo && sp < hi && aligned(sp);
63+
}
64+
5565
// Drop unknown leaf frame (method_id == NULL at index 0).
5666
// Returns the new depth after removal.
5767
static inline int dropUnknownLeaf(ASGCT_CallFrame* frames, int depth) {

0 commit comments

Comments
 (0)