From d29aa9ec6fa3161cda71421f738df80675bd2ece Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 3 Apr 2026 13:01:57 +0200 Subject: [PATCH 1/5] fix(wallclock): remove duplicate thread_list->next() call in timerLoop With filter= (all-threads mode) the loop called next() twice per iteration, skipping every other thread. Remove the spurious second call. --- ddprof-lib/src/main/cpp/wallClock.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/ddprof-lib/src/main/cpp/wallClock.cpp b/ddprof-lib/src/main/cpp/wallClock.cpp index 6150f50bc..50c4d85a1 100644 --- a/ddprof-lib/src/main/cpp/wallClock.cpp +++ b/ddprof-lib/src/main/cpp/wallClock.cpp @@ -155,7 +155,6 @@ void WallClockASGCT::timerLoop() { if (tid != OS::threadId()) { tids.push_back(tid); } - tid = thread_list->next(); } delete thread_list; } From 275b8c87bd61d78d620d3de568a9b8f4ea97ac14 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 3 Apr 2026 13:02:08 +0200 Subject: [PATCH 2/5] feat(walkvm): unwind through virtual thread continuation boundaries On JDK 21+ with cstack=vmx, walkVM now crosses the continuation boundary to include carrier-thread (ForkJoinWorkerThread) frames in wall-clock profiles of virtual threads. Two detection paths: - Thawed boundary: cont_entry_return_pc is the return PC of the bottommost thawed frame; traverse enterSpecial to reach the carrier stack. - Frozen boundary: cont_returnBarrier is the return PC when frozen frames remain in the StackChunk; switch to the carrier stack directly. Both addresses are resolved via gHotSpotVMStructs (JDK 27+) or C++ symbol lookup fallback (JDK 21-26). A speculative fallback fires when neither boundary is detected but isCarryingVirtualThread() confirms a mounted VT. carrier_frames is now enabled automatically with cstack=vmx. The wextend=vt_carrier option is removed. --- ddprof-lib/src/main/cpp/arguments.cpp | 4 +- ddprof-lib/src/main/cpp/arguments.h | 3 +- ddprof-lib/src/main/cpp/counters.h | 6 +- ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp | 43 +++++ ddprof-lib/src/main/cpp/hotspot/vmStructs.h | 109 +++++++++++-- ddprof-lib/src/main/cpp/stackWalker.cpp | 150 +++++++++++++++++- ddprof-lib/src/main/cpp/stackWalker.h | 10 ++ ddprof-lib/src/test/cpp/stackWalker_ut.cpp | 59 +++++++ 8 files changed, 368 insertions(+), 16 deletions(-) diff --git a/ddprof-lib/src/main/cpp/arguments.cpp b/ddprof-lib/src/main/cpp/arguments.cpp index 72b8aec22..708263e54 100644 --- a/ddprof-lib/src/main/cpp/arguments.cpp +++ b/ddprof-lib/src/main/cpp/arguments.cpp @@ -292,9 +292,11 @@ Error Arguments::parse(const char *args) { } else if (strcmp(value, "vm") == 0) { _cstack = CSTACK_VM; } else if (strcmp(value, "vmx") == 0) { - // cstack=vmx is a shorthand for cstack=vm,features=mixed + // cstack=vmx is a shorthand for cstack=vm,features=mixed; carrier-frame + // unwinding is enabled automatically since vmx already traverses entry frames _cstack = CSTACK_VM; _features.mixed = 1; + _features.carrier_frames = 1; } else { _cstack = CSTACK_NO; } diff --git a/ddprof-lib/src/main/cpp/arguments.h b/ddprof-lib/src/main/cpp/arguments.h index e80dcbd52..1d21cbf2b 100644 --- a/ddprof-lib/src/main/cpp/arguments.h +++ b/ddprof-lib/src/main/cpp/arguments.h @@ -122,7 +122,8 @@ struct StackWalkFeatures { unsigned short vtable_target : 1; // show receiver classes of vtable/itable stubs unsigned short comp_task : 1; // display current compilation task for JIT threads unsigned short pc_addr : 1; // record exact PC address for each sample - unsigned short _padding : 3; // pad structure to 16 bits + unsigned short carrier_frames: 1; // walk through VT continuation boundary to carrier frames (enabled automatically with cstack=vmx) + unsigned short _padding : 2; // pad structure to 16 bits }; struct Multiplier { diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index 275149cac..f6616311c 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -92,7 +92,11 @@ X(WALKVM_STUB_FRAMESIZE_FALLBACK, "walkvm_stub_framesize_fallback") \ X(WALKVM_FP_CHAIN_ATTEMPT, "walkvm_fp_chain_attempt") \ X(WALKVM_FP_CHAIN_REACHED_CODEHEAP, "walkvm_fp_chain_reached_codeheap") \ - X(WALKVM_ANCHOR_NOT_IN_JAVA, "walkvm_anchor_not_in_java") \ + X(WALKVM_ANCHOR_NOT_IN_JAVA, "walkvm_anchor_not_in_java") \ + X(WALKVM_CONT_BARRIER_HIT, "walkvm_cont_barrier_hit") \ + X(WALKVM_ENTER_SPECIAL_HIT, "walkvm_enter_special_hit") \ + X(WALKVM_CONT_SPECULATIVE_HIT,"walkvm_cont_speculative_hit") \ + X(WALKVM_CONT_ENTRY_NULL, "walkvm_cont_entry_null") \ X(NATIVE_LIBS_DROPPED, "native_libs_dropped") \ X(SIGACTION_PATCHED_LIBS, "sigaction_patched_libs") \ X(SIGACTION_INTERCEPTED, "sigaction_intercepted") diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp index 3a3c4c27b..f7b4eed9a 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp @@ -35,6 +35,9 @@ int VMStructs::_narrow_klass_shift = -1; int VMStructs::_interpreter_frame_bcp_offset = 0; unsigned char VMStructs::_unsigned5_base = 0; const void* VMStructs::_call_stub_return = nullptr; +const void* VMStructs::_cont_return_barrier = nullptr; +const void* VMStructs::_cont_entry_return_pc = nullptr; +VMNMethod* VMStructs::_enter_special_nm = nullptr; const void* VMStructs::_interpreter_start = nullptr; VMNMethod* VMStructs::_interpreter_nm = nullptr; const void* VMStructs::_interpreted_frame_valid_start = nullptr; @@ -44,6 +47,7 @@ const void* VMStructs::_interpreted_frame_valid_end = nullptr; // Initialize type size to 0 #define INIT_TYPE_SIZE(name, names) uint64_t VMStructs::TYPE_SIZE_NAME(name) = 0; DECLARE_TYPES_DO(INIT_TYPE_SIZE) +DECLARE_V21_TYPES_DO(INIT_TYPE_SIZE) #undef INIT_TYPE_SIZE #define offset_value -1 @@ -62,6 +66,7 @@ DECLARE_TYPES_DO(INIT_TYPE_SIZE) field_type VMStructs::var = field_type##_value; DECLARE_TYPE_FIELD_DO(DO_NOTHING, INIT_FIELD, INIT_FIELD_WITH_VERSION, DO_NOTHING) +DECLARE_V21_TYPE_FIELD_DO(DO_NOTHING, INIT_FIELD, INIT_FIELD_WITH_VERSION, DO_NOTHING) #undef INIT_FIELD #undef INIT_FIELD_WITH_VERSION @@ -175,6 +180,7 @@ void VMStructs::init_offsets_and_addresses() { #define END_TYPE() continue; } DECLARE_TYPE_FIELD_DO(MATCH_TYPE_NAMES, READ_FIELD_VALUE, READ_FIELD_VALUE_WITH_VERSION, END_TYPE) + DECLARE_V21_TYPE_FIELD_DO(MATCH_TYPE_NAMES, READ_FIELD_VALUE, READ_FIELD_VALUE_WITH_VERSION, END_TYPE) #undef MATCH_TYPE_NAMES #undef READ_FIELD_VALUE #undef READ_FIELD_VALUE_WITH_VERSION @@ -205,6 +211,7 @@ void VMStructs::init_type_sizes() { } DECLARE_TYPES_DO(READ_TYPE_SIZE) + DECLARE_V21_TYPES_DO(READ_TYPE_SIZE) #undef READ_TYPE_SIZE @@ -271,12 +278,21 @@ void VMStructs::verify_offsets() { } // Verify type sizes +// Note: DECLARE_V21_TYPES_DO (VMContinuationEntry) is intentionally excluded here. +// ContinuationEntry is not exported in gHotSpotVMTypes before JDK 27 (added via JDK-8378985); +// asserting type_size() > 0 would SIGABRT on any JDK 21-26 build. #define VERIFY_TYPE_SIZE(name, names) assert(TYPE_SIZE_NAME(name) > 0); DECLARE_TYPES_DO(VERIFY_TYPE_SIZE); #undef VERIFY_TYPE_SIZE // Verify offsets and addresses +// Note: DECLARE_V21_TYPE_FIELD_DO is intentionally excluded here. +// Continuation-related fields (_cont_entry_offset, _cont_return_barrier_addr, +// _cont_entry_return_pc_addr, _cont_entry_parent_offset) are absent from +// gHotSpotVMStructs in all JDK 21-26 builds: ContinuationEntry was not +// exported in the vmStructs table until JDK 27 (JDK-8378985). walkVM degrades +// gracefully when they are missing. #define offset_value -1 #define address_value nullptr @@ -391,6 +407,25 @@ void VMStructs::resolveOffsets() { if (_call_stub_return_addr != NULL) { _call_stub_return = *(const void**)_call_stub_return_addr; } + if (_cont_return_barrier_addr != NULL) { + _cont_return_barrier = *(const void**)_cont_return_barrier_addr; + } + if (_cont_entry_return_pc_addr != NULL) { + _cont_entry_return_pc = *(const void**)_cont_entry_return_pc_addr; + } + // Fallback for JDK 21-26: StubRoutines::_cont_returnBarrier and + // ContinuationEntry::_return_pc are absent from gHotSpotVMStructs before + // JDK 27 (added via JDK-8378985). Resolve them via C++ symbol lookup. + // Symbol names use Itanium C++ ABI mangling (GCC/Clang), which matches + // the HotSpot build toolchain on all supported platforms. + if (_cont_return_barrier == nullptr && VM::hotspot_version() >= 21) { + const void** sym = (const void**)_libjvm->findSymbol("_ZN12StubRoutines19_cont_returnBarrierE"); + if (sym != nullptr) _cont_return_barrier = *sym; + } + if (_cont_entry_return_pc == nullptr && VM::hotspot_version() >= 21) { + const void** sym = (const void**)_libjvm->findSymbol("_ZN17ContinuationEntry10_return_pcE"); + if (sym != nullptr) _cont_entry_return_pc = *sym; + } // Since JDK 23, _metadata_offset is relative to _data_offset. See metadata() if (_nmethod_immutable_offset < 0) { @@ -440,6 +475,14 @@ void VMStructs::resolveOffsets() { if (_interpreter_nm == NULL && _interpreter_start != NULL) { _interpreter_nm = CodeHeap::findNMethod(_interpreter_start); } + if (_enter_special_nm == NULL && _cont_entry_return_pc != NULL) { + // On JDK 27+, enterSpecial is a proper nmethod; findNMethod succeeds. + // On JDK 21-26, it is a RuntimeBlob; findNMethod returns NULL and + // _enter_special_nm stays NULL. The cont_entry_return_pc boundary is + // then detected via isContEntryReturnPc() in the walk loop rather than + // nmethod identity. + _enter_special_nm = CodeHeap::findNMethod(_cont_entry_return_pc); + } } void VMStructs::initJvmFunctions() { diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h index 418907a98..a3cf148dd 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h @@ -116,16 +116,38 @@ inline T* cast_to(const void* ptr) { */ #define DECLARE_TYPES_DO(f) \ - f(VMClassLoaderData, MATCH_SYMBOLS("ClassLoaderData")) \ - f(VMConstantPool, MATCH_SYMBOLS("ConstantPool")) \ - f(VMConstMethod, MATCH_SYMBOLS("ConstMethod")) \ - f(VMFlag, MATCH_SYMBOLS("JVMFlag", "Flag")) \ - f(VMJavaFrameAnchor, MATCH_SYMBOLS("JavaFrameAnchor")) \ - f(VMKlass, MATCH_SYMBOLS("Klass")) \ - f(VMMethod, MATCH_SYMBOLS("Method")) \ - f(VMNMethod, MATCH_SYMBOLS("nmethod")) \ - f(VMSymbol, MATCH_SYMBOLS("Symbol")) \ - f(VMThread, MATCH_SYMBOLS("Thread")) + f(VMClassLoaderData, MATCH_SYMBOLS("ClassLoaderData")) \ + f(VMConstantPool, MATCH_SYMBOLS("ConstantPool")) \ + f(VMConstMethod, MATCH_SYMBOLS("ConstMethod")) \ + f(VMFlag, MATCH_SYMBOLS("JVMFlag", "Flag")) \ + f(VMJavaFrameAnchor, MATCH_SYMBOLS("JavaFrameAnchor")) \ + f(VMKlass, MATCH_SYMBOLS("Klass")) \ + f(VMMethod, MATCH_SYMBOLS("Method")) \ + f(VMNMethod, MATCH_SYMBOLS("nmethod")) \ + f(VMSymbol, MATCH_SYMBOLS("Symbol")) \ + f(VMThread, MATCH_SYMBOLS("Thread")) + +// Types only present in JDK 21+ (Project Loom); size is 0 on older JDKs +#define DECLARE_V21_TYPES_DO(f) \ + f(VMContinuationEntry, MATCH_SYMBOLS("ContinuationEntry")) + +// Fields for JDK 21+ virtual-thread / continuation support. +// ContinuationEntry was not exported in gHotSpotVMStructs until JDK 27 +// (JDK-8378985), so these fields are absent from the table in all JDK 21-26 +// builds and are populated via C++ mangled-symbol fallback instead. They are +// intentionally excluded from verify_offsets() so that a missing entry causes +// graceful degradation rather than SIGABRT. +#define DECLARE_V21_TYPE_FIELD_DO(type_begin, field, field_with_version, type_end) \ + type_begin(VMJavaThread, MATCH_SYMBOLS("JavaThread", "Thread")) \ + field_with_version(_cont_entry_offset, offset, 21, MAX_VERSION, MATCH_SYMBOLS("_cont_entry")) \ + type_end() \ + type_begin(VMStubRoutine, MATCH_SYMBOLS("StubRoutines")) \ + field_with_version(_cont_return_barrier_addr, address, 21, MAX_VERSION, MATCH_SYMBOLS("_cont_returnBarrier")) \ + type_end() \ + type_begin(VMContinuationEntry, MATCH_SYMBOLS("ContinuationEntry")) \ + field_with_version(_cont_entry_return_pc_addr, address, 21, MAX_VERSION, MATCH_SYMBOLS("_return_pc")) \ + field_with_version(_cont_entry_parent_offset, offset, 21, MAX_VERSION, MATCH_SYMBOLS("_parent")) \ + type_end() /** * Following macros define field offsets, addresses or values of JVM classes that are exported by @@ -250,7 +272,7 @@ typedef void* address; field(_vs_high_offset, offset, MATCH_SYMBOLS("_high")) \ type_end() \ type_begin(VMStubRoutine, MATCH_SYMBOLS("StubRoutines")) \ - field(_call_stub_return_addr, address, MATCH_SYMBOLS("_call_stub_return_address")) \ + field(_call_stub_return_addr, address, MATCH_SYMBOLS("_call_stub_return_address")) \ type_end() \ type_begin(VMGrowableArray, MATCH_SYMBOLS("GrowableArrayBase", "GenericGrowableArray")) \ field(_array_len_offset, offset, MATCH_SYMBOLS("_len")) \ @@ -313,6 +335,9 @@ class VMStructs { static int _interpreter_frame_bcp_offset; static unsigned char _unsigned5_base; static const void* _call_stub_return; + static const void* _cont_return_barrier; + static const void* _cont_entry_return_pc; + static VMNMethod* _enter_special_nm; static const void* _interpreter_start; static VMNMethod* _interpreter_nm; static const void* _interpreted_frame_valid_start; @@ -324,6 +349,7 @@ class VMStructs { static uint64_t TYPE_SIZE_NAME(name); DECLARE_TYPES_DO(DECLARE_TYPE_SIZE_VAR) + DECLARE_V21_TYPES_DO(DECLARE_TYPE_SIZE_VAR) #undef DECLARE_TYPE_SIZE_VAR // Declare vmStructs' field offsets and addresses @@ -336,6 +362,7 @@ class VMStructs { static field_type var; DECLARE_TYPE_FIELD_DO(DO_NOTHING, DECLARE_TYPE_FIELD, DECLARE_TYPE_FIELD_WITH_VERSION, DO_NOTHING) + DECLARE_V21_TYPE_FIELD_DO(DO_NOTHING, DECLARE_TYPE_FIELD, DECLARE_TYPE_FIELD_WITH_VERSION, DO_NOTHING) #undef DECLARE_TYPE_FIELD #undef DECLARE_TYPE_FIELD_WITH_VERSION #undef DO_NOTHING @@ -448,6 +475,21 @@ class VMStructs { return pc >= _interpreted_frame_valid_start && pc < _interpreted_frame_valid_end; } + static bool isContReturnBarrier(const void* pc) { + return _cont_return_barrier != nullptr && pc == _cont_return_barrier; + } + + // True when the bottom VT frame's return PC is cont_entry_return_pc, meaning all + // VT frames are thawed (CPU-bound VT that never yielded). + // Available on JDK 21+ via vmStructs or symbol fallback. + static bool isContEntryReturnPc(const void* pc) { + return _cont_entry_return_pc != nullptr && pc == _cont_entry_return_pc; + } + + static VMNMethod* enterSpecialNMethod() { + return _enter_special_nm; + } + // Datadog-specific extensions static bool isSafeToWalk(uintptr_t pc); static void JNICALL NativeMethodBind(jvmtiEnv *jvmti, JNIEnv *jni, @@ -675,6 +717,30 @@ DECLARE(VMJavaFrameAnchor) } DECLARE_END +DECLARE(VMContinuationEntry) + public: + // Address of the enterSpecial frame's {saved_fp, return_addr} pair. + // Layout above this address: [saved_fp][return_addr_to_carrier][carrier_sp...] + // The ContinuationEntry struct is embedded on the carrier stack immediately + // below enterSpecial's saved-fp slot; its size() equals the JVM's + // ContinuationEntry::size() static method, confirmed at: + // https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/continuationEntry.hpp + // https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/continuationEntry.cpp + uintptr_t entryFP() const { + assert(type_size() > 0); // must not be called before ContinuationEntry is resolved + return (uintptr_t)this + type_size(); + } + + // Returns the enclosing ContinuationEntry when continuations are nested + // (e.g. a Continuation.run() call inside a virtual thread). Returns + // nullptr when there is no enclosing entry or the field is unavailable. + VMContinuationEntry* parent() const { + if (_cont_entry_parent_offset < 0) return nullptr; + void* ptr = SafeAccess::loadPtr((void**) const_cast(this)->at(_cont_entry_parent_offset), nullptr); + return ptr != nullptr ? VMContinuationEntry::cast(ptr) : nullptr; + } +DECLARE_END + // Copied from JDK's globalDefinitions.hpp 'JavaThreadState' enum enum JVMJavaThreadState { _thread_uninitialized = 0, // should never happen (missing initialization) @@ -793,6 +859,27 @@ DECLARE(VMThread) return VMJavaFrameAnchor::cast(at(_thread_anchor_offset)); } + // Returns true when this thread is currently executing a virtual thread + // (i.e. JavaThread::_cont_entry is non-null). Available on all JDK 21+ + // builds because _cont_entry is a field of JavaThread, which is always + // exported in gHotSpotVMStructs. Does NOT require ContinuationEntry type + // size, so it works on JDK 21-26 where type_size() == 0. + bool isCarryingVirtualThread() const { + if (_cont_entry_offset < 0) return false; + return SafeAccess::loadPtr((void**) const_cast(this)->at(_cont_entry_offset), nullptr) != nullptr; + } + + // Returns the innermost active ContinuationEntry for this thread, or nullptr + // if none exists or ContinuationEntry layout is unavailable (JDK 21-26, + // where ContinuationEntry is not in gHotSpotVMTypes so type_size() == 0). + // Used by stackWalker to locate the enterSpecial frame when crossing the + // virtual-thread continuation boundary. + VMContinuationEntry* contEntry() { + if (_cont_entry_offset < 0 || VMContinuationEntry::type_size() == 0) return nullptr; + void* ptr = SafeAccess::loadPtr((void**) at(_cont_entry_offset), nullptr); + return ptr != nullptr ? VMContinuationEntry::cast(ptr) : nullptr; + } + inline VMMethod* compiledMethod(); private: static inline int nativeThreadId(JNIEnv* jni, jthread thread); diff --git a/ddprof-lib/src/main/cpp/stackWalker.cpp b/ddprof-lib/src/main/cpp/stackWalker.cpp index 93448a87f..932376d55 100644 --- a/ddprof-lib/src/main/cpp/stackWalker.cpp +++ b/ddprof-lib/src/main/cpp/stackWalker.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include #include "stackWalker.h" #include "dwarf.h" @@ -19,6 +20,19 @@ const uintptr_t MAX_WALK_SIZE = 0x100000; const intptr_t MAX_FRAME_SIZE_WORDS = StackWalkValidation::MAX_FRAME_SIZE / sizeof(void*); // 0x8000 = 32768 words +#ifdef NDEBUG +static const bool CONT_UNWIND_DISABLED = false; +#else +// DEBUG-only: when set, both continuation-unwind detection branches +// (cont_entry_return_pc for fully-thawed VTs, cont_returnBarrier for VTs +// with frozen frames) are skipped, reproducing pre-fix behaviour. +// Used by negative integration tests to verify that carrier frames are not +// visible and walk-error sentinels do appear without the fix. +// NOTE: the env var is evaluated once at library load time; it must be set +// in the environment before the profiler agent is attached. +static const bool CONT_UNWIND_DISABLED = (std::getenv("DDPROF_DISABLE_CONT_UNWIND") != nullptr); +#endif + static ucontext_t empty_ucontext{}; // Use validation helpers from header (shared with tests) @@ -323,6 +337,9 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext, const void* prev_native_pc = NULL; + // Last ContinuationEntry crossed; advanced via parent() for nested continuations. + VMContinuationEntry* cont_entry = nullptr; + // Saved anchor data — preserved across anchor consumption so inline // recovery can redirect even after the anchor pointer has been set to NULL. // Recovery is one-shot: once attempted, we do not retry to avoid @@ -339,6 +356,80 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext, anchor = vm_thread->anchor(); } + static const char* CONT_ROOT_FRAME = "JVM Continuation"; + + // Advances through a continuation boundary to the carrier frame. + // Without carrier_frames (default, cstack=vm): always stops with a "JVM Continuation" + // synthetic root frame — VT frames are complete, carrier internals are noise. + // With carrier_frames (cstack=vmx): attempts to walk through; failures emit BCI_ERROR + // so the sample is truthfully marked truncated. + // Walks cont_entry->parent() on repeated calls to handle nested continuations + // (_parent not triggered by standard single-level VTs today, but required + // once any runtime layers continuations on top of VTs). + // + // all_frames_thawed: true when the bottom VT frame's return PC is + // cont_entry_return_pc (all VT frames are thawed — CPU-bound VT), + // false when it is cont_returnBarrier (frozen frames remain in the + // StackChunk — VT parked and just remounted). + // Needed to derive entry_fp on JDK 21-26 where ContinuationEntry + // type size is absent from vmStructs and contEntry() returns nullptr. + // + // Returns true to continue the walk, false to break. + auto walkThroughContinuation = [&](bool all_frames_thawed) -> bool { + if (depth >= actual_max_depth) return false; + if (!features.carrier_frames) { + fillFrame(frames[depth++], BCI_NATIVE_FRAME, CONT_ROOT_FRAME); + return false; + } + + uintptr_t entry_fp; + + if (VMContinuationEntry::type_size() > 0) { + // ContinuationEntry is known via vmStructs (JDK 27+, and JDK 21-26 + // on distros that expose it). Walk the linked list of entries for + // nested-continuation support and derive the enterSpecial frame FP + // from the struct layout (entry + type_size). + cont_entry = (cont_entry != nullptr) ? cont_entry->parent() : vm_thread->contEntry(); + if (cont_entry == nullptr) { + Counters::increment(WALKVM_CONT_ENTRY_NULL); + fillFrame(frames[depth++], BCI_ERROR, "break_cont_entry_null"); + return false; + } + entry_fp = cont_entry->entryFP(); + } else { + // ContinuationEntry absent from vmStructs (musl/minimal JDK 21-26). + // Derive the enterSpecial frame FP from the current fp: + // all frames thawed (pc == cont_entry_return_pc): fp IS the + // enterSpecial frame FP. + // frozen frames remain (pc == cont_returnBarrier): the saved + // caller FP at *fp leads to the enterSpecial frame on the + // carrier stack. + // Nested continuation tracking is unavailable without type_size(). + entry_fp = all_frames_thawed ? fp : (uintptr_t)SafeAccess::load((void**)fp); + } + + if (!StackWalkValidation::isValidFP(entry_fp)) { + fillFrame(frames[depth++], BCI_ERROR, "break_cont_entry_fp"); + return false; + } + // entry_fp has been range-checked by isValidFP above; any remaining + // SIGSEGV from a stale/concurrently-freed pointer is caught by the + // setjmp crash protection in walkVM (checkFault -> longjmp). + uintptr_t carrier_fp = *(uintptr_t*)entry_fp; + const void* carrier_pc = ((const void**)entry_fp)[FRAME_PC_SLOT]; + uintptr_t carrier_sp = entry_fp + (FRAME_PC_SLOT + 1) * sizeof(void*); + if (!StackWalkValidation::isValidFP(carrier_fp) || + StackWalkValidation::inDeadZone(carrier_pc) || + !StackWalkValidation::isValidSP(carrier_sp, sp, bottom)) { + fillFrame(frames[depth++], BCI_ERROR, "break_cont_carrier_sp"); + return false; + } + sp = carrier_sp; + fp = carrier_fp; + pc = carrier_pc; + return true; + }; + unwind_loop: // Walk until the bottom of the stack or until the first Java frame @@ -369,8 +460,44 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext, break; } prev_native_pc = NULL; // we are in JVM code, no previous 'native' PC + + // Both continuation boundary PCs are JVM stubs whose findNMethod() + // returns NULL; detect them by exact-PC match before the nmethod + // dispatch below. + // cont_returnBarrier: bottom thawed frame returns here when frozen + // frames remain in the StackChunk (blocking/remounted VT). + // cont_entry_return_pc: bottom thawed frame returns here when the + // continuation is fully thawed (CPU-bound VT, never yielded). + if (!CONT_UNWIND_DISABLED && VMStructs::isContReturnBarrier(pc)) { + Counters::increment(WALKVM_CONT_BARRIER_HIT); + if (walkThroughContinuation(false)) continue; + break; + } + if (!CONT_UNWIND_DISABLED && VMStructs::isContEntryReturnPc(pc)) { + Counters::increment(WALKVM_ENTER_SPECIAL_HIT); + if (walkThroughContinuation(true)) continue; + break; + } + VMNMethod* nm = CodeHeap::findNMethod(pc); if (nm == NULL) { + // On JDK 21+ builds, the continuation entry PC may be absent + // from vmStructs OR resolved but pointing to the wrong address + // (some distributions expose the symbol at the wrong address, so + // the exact-PC check above never fires). Attempt a fully-thawed + // continuation walk whenever we see an unknown nmethod after + // collecting Java frames. walkThroughContinuation validates the + // fp chain and emits BCI_ERROR cleanly on mismatch, so false + // positives are safe. + if (!CONT_UNWIND_DISABLED + && features.carrier_frames + && VM::hotspot_version() >= 21 + && depth > 0 + && vm_thread != NULL && vm_thread->isCarryingVirtualThread()) { + Counters::increment(WALKVM_CONT_SPECULATIVE_HIT); + if (walkThroughContinuation(true)) continue; + break; + } if (anchor == NULL) { // Add an error frame only if we cannot recover fillFrame(frames[depth++], BCI_ERROR, "unknown_nmethod"); @@ -381,7 +508,13 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext, // Always prefer JavaFrameAnchor when it is available, // since it provides reliable SP and FP. // Do not treat the topmost stub as Java frame. - if (anchor != NULL && (depth > 0 || !nm->isStub())) { + // Exception: when VT carrier-frame unwinding is active, skip the anchor + // redirect — it can bypass the continuation boundary by jumping directly + // into carrier frames, causing walkThroughContinuation to never fire. + // The continuation mechanism finds carrier frames on its own. + bool anchor_eligible = anchor != NULL && (depth > 0 || !nm->isStub()); + bool cont_unwind_active = features.carrier_frames && !CONT_UNWIND_DISABLED; + if (anchor_eligible && !cont_unwind_active) { Counters::increment(WALKVM_ANCHOR_CONSUMED); // Preserve anchor data before consumption — getFrame() is read-only // but we set anchor=NULL below, losing the pointer for later recovery. @@ -395,6 +528,10 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext, continue; // NMethod has changed as a result of correction } anchor = NULL; + } else if (anchor_eligible && cont_unwind_active) { + // Clear the anchor without redirecting so it doesn't corrupt fp + // for the continuation boundary walk. + anchor = NULL; } if (nm->isInterpreter()) { @@ -444,6 +581,15 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext, fillFrame(frames[depth++], BCI_ERROR, "break_interpreted"); break; } else if (nm->isNMethod()) { + // enterSpecial is a generated native nmethod that acts as the + // continuation entry stub. It has no JavaCallWrapper, so + // isEntryFrame() will not fire for it. Detect it by identity + // and navigate to the carrier thread via ContinuationEntry. + if (!CONT_UNWIND_DISABLED && nm == VMStructs::enterSpecialNMethod()) { + Counters::increment(WALKVM_ENTER_SPECIAL_HIT); + if (walkThroughContinuation(true)) continue; + break; + } // Check if deoptimization is in progress before walking compiled frames if (vm_thread != NULL && vm_thread->inDeopt()) { fillFrame(frames[depth++], BCI_ERROR, "break_deopt_compiled"); @@ -502,7 +648,7 @@ __attribute__((no_sanitize("address"))) int StackWalker::walkVM(void* ucontext, Counters::increment(WALKVM_BREAK_COMPILED); fillFrame(frames[depth++], BCI_ERROR, "break_compiled"); break; - } else if (nm->isEntryFrame(pc) && !features.mixed) { + } else if (nm->isEntryFrame(pc) && (!features.mixed || features.carrier_frames)) { VMJavaFrameAnchor* next_anchor = VMJavaFrameAnchor::fromEntryFrame(fp); if (next_anchor == NULL) { fillFrame(frames[depth++], BCI_ERROR, "break_entry_frame"); diff --git a/ddprof-lib/src/main/cpp/stackWalker.h b/ddprof-lib/src/main/cpp/stackWalker.h index 10b91e0c4..dddb593e5 100644 --- a/ddprof-lib/src/main/cpp/stackWalker.h +++ b/ddprof-lib/src/main/cpp/stackWalker.h @@ -52,6 +52,16 @@ namespace StackWalkValidation { return (uintptr_t)hi - (uintptr_t)lo < SAME_STACK_DISTANCE; } + // Check if a frame pointer is plausibly valid (not in dead zone, properly aligned) + static inline bool isValidFP(uintptr_t fp) { + return !inDeadZone((const void*)fp) && aligned(fp); + } + + // Check if a stack pointer is within [lo, hi) and properly aligned + static inline bool isValidSP(uintptr_t sp, uintptr_t lo, uintptr_t hi) { + return sp > lo && sp < hi && aligned(sp); + } + // Drop unknown leaf frame (method_id == NULL at index 0). // Returns the new depth after removal. static inline int dropUnknownLeaf(ASGCT_CallFrame* frames, int depth) { diff --git a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp index 7017c8092..b0230bd2a 100644 --- a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp +++ b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp @@ -82,3 +82,62 @@ TEST_F(StackWalkerTest, dropUnknownLeaf_unknown_non_leaf_not_dropped) { EXPECT_EQ(nullptr, frames[1].method_id); EXPECT_NE(nullptr, frames[2].method_id); } + +// ---- isValidFP ---- + +TEST_F(StackWalkerTest, isValidFP_null_is_invalid) { + EXPECT_FALSE(StackWalkValidation::isValidFP(0)); +} + +TEST_F(StackWalkerTest, isValidFP_low_address_is_invalid) { + EXPECT_FALSE(StackWalkValidation::isValidFP(0x100)); // below DEAD_ZONE (0x1000) + EXPECT_FALSE(StackWalkValidation::isValidFP(0xfff)); +} + +TEST_F(StackWalkerTest, isValidFP_high_address_is_invalid) { + // Within DEAD_ZONE of UINTPTR_MAX (i.e. >= -DEAD_ZONE) + EXPECT_FALSE(StackWalkValidation::isValidFP(~(uintptr_t)0)); // UINTPTR_MAX + EXPECT_FALSE(StackWalkValidation::isValidFP(~(uintptr_t)0 - 0x100)); // still in dead zone +} + +TEST_F(StackWalkerTest, isValidFP_misaligned_is_invalid) { + // Aligned address in valid range but with low bits set + EXPECT_FALSE(StackWalkValidation::isValidFP(0x10001)); // odd + EXPECT_FALSE(StackWalkValidation::isValidFP(0x10002)); // 2-byte aligned but not pointer-aligned +} + +TEST_F(StackWalkerTest, isValidFP_valid_aligned_address) { + // Aligned addresses well within valid range should pass + EXPECT_TRUE(StackWalkValidation::isValidFP(0x10000)); + EXPECT_TRUE(StackWalkValidation::isValidFP(0x7fff0000)); +} + +// ---- isValidSP ---- + +TEST_F(StackWalkerTest, isValidSP_must_be_strictly_above_lo) { + uintptr_t lo = 0x1000; + uintptr_t hi = 0x5000; + EXPECT_FALSE(StackWalkValidation::isValidSP(lo, lo, hi)); // sp == lo: not strictly above + EXPECT_FALSE(StackWalkValidation::isValidSP(lo - 8, lo, hi)); // sp < lo +} + +TEST_F(StackWalkerTest, isValidSP_must_be_strictly_below_hi) { + uintptr_t lo = 0x1000; + uintptr_t hi = 0x5000; + EXPECT_FALSE(StackWalkValidation::isValidSP(hi, lo, hi)); // sp == hi: not strictly below + EXPECT_FALSE(StackWalkValidation::isValidSP(hi + 8, lo, hi)); // sp > hi +} + +TEST_F(StackWalkerTest, isValidSP_misaligned_is_invalid) { + uintptr_t lo = 0x1000; + uintptr_t hi = 0x5000; + EXPECT_FALSE(StackWalkValidation::isValidSP(0x2001, lo, hi)); // in range but misaligned +} + +TEST_F(StackWalkerTest, isValidSP_valid_aligned_in_range) { + uintptr_t lo = 0x1000; + uintptr_t hi = 0x5000; + EXPECT_TRUE(StackWalkValidation::isValidSP(0x2000, lo, hi)); + EXPECT_TRUE(StackWalkValidation::isValidSP(lo + 8, lo, hi)); + EXPECT_TRUE(StackWalkValidation::isValidSP(hi - 8, lo, hi)); +} From 364a85da569f2bac29128fd0a725380c9d7199e8 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 3 Apr 2026 13:07:49 +0200 Subject: [PATCH 3/5] fix(alloc): append synthetic JVM Continuation root frame for VT allocation samples GetStackTrace on a virtual thread stops at the continuation boundary and never includes carrier frames. Without a synthetic root the trace appears truncated to the UI backend ("Missing Frames"). Append a BCI_NATIVE_FRAME named "JVM Continuation" when isCarryingVirtualThread() is true on the carrier. Same fix applied to the ASGCT path (fp/dwarf cstack modes): AsyncGetCallTrace also stops at the continuation boundary for mounted virtual threads. --- ddprof-lib/src/main/cpp/profiler.cpp | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index a081ad9ca..12b63c991 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -15,6 +15,7 @@ #include "flightRecorder.h" #include "itimer.h" #include "hotspot/vmStructs.h" +#include "hotspot/vmStructs.inline.h" #include "j9/j9Ext.h" #include "j9/j9WallClock.h" #include "jvmThread.h" @@ -791,6 +792,24 @@ u64 Profiler::recordJVMTISample(u64 counter, int tid, jthread thread, jint event // see https://github.com/async-profiler/async-profiler/pull/1090 LP64_ONLY(frames[i].padding = 0;) } + // On JDK 21+, GetStackTrace on a virtual thread returns only the VT's + // logical stack; it stops at the continuation boundary and never includes + // carrier-thread frames. Without a synthetic root the trace appears + // truncated to the UI backend, which attributes it to "Missing Frames". + // Detect the VT case via JavaThread::_cont_entry being non-null on the + // carrier. This field is in gHotSpotVMStructs on all JDK 21+ builds so + // isCarryingVirtualThread() works regardless of JDK version. Append a + // synthetic "JVM Continuation" root frame to mark the boundary + // explicitly, matching the behaviour of walkVM without carrier_frames. + if (VM::hotspot_version() >= 21 && num_frames < _max_stack_depth) { + VMThread* carrier = VMThread::current(); + if (carrier != nullptr && carrier->isCarryingVirtualThread()) { + frames[num_frames].bci = BCI_NATIVE_FRAME; + frames[num_frames].method_id = (jmethodID) "JVM Continuation"; + LP64_ONLY(frames[num_frames].padding = 0;) + num_frames++; + } + } } call_trace_id = _call_trace_storage.put(num_frames, frames, false, counter); @@ -884,6 +903,17 @@ void Profiler::recordSample(void *ucontext, u64 counter, int tid, } } num_frames += java_frames; + // ASGCT stops at the continuation boundary for virtual threads (JDK 21+). + // Append a synthetic root frame so the UI does not show "Missing Frames". + if (java_frames > 0 && VM::hotspot_version() >= 21 && num_frames < _max_stack_depth) { + VMThread* carrier = VMThread::current(); + if (carrier != nullptr && carrier->isCarryingVirtualThread()) { + frames[num_frames].bci = BCI_NATIVE_FRAME; + frames[num_frames].method_id = (jmethodID) "JVM Continuation"; + LP64_ONLY(frames[num_frames].padding = 0;) + num_frames++; + } + } } } } From c21fa788e08b376a5a4b2917d4a41faafefef6cc Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 3 Apr 2026 13:26:49 +0200 Subject: [PATCH 4/5] test(vt): add carrier-frame wall-clock integration test VirtualThreadWallClockTest exercises both continuation unwind paths with cstack=vmx: - samplesCarrierFramesFromCpuBoundVT: all frames thawed (cont_entry_return_pc boundary) - samplesCarrierFramesFromBlockingVT: frozen frames in StackChunk (cont_returnBarrier boundary) Both tests assert ForkJoinWorkerThread carrier frames appear in wall-clock samples. Skipped on non-vmx cstack modes and JDK < 21 via assumeTrue. --- .../wallclock/VirtualThreadWallClockTest.java | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/VirtualThreadWallClockTest.java diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/VirtualThreadWallClockTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/VirtualThreadWallClockTest.java new file mode 100644 index 000000000..e4a51ac3c --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/VirtualThreadWallClockTest.java @@ -0,0 +1,262 @@ +package com.datadoghq.profiler.wallclock; + +import com.datadoghq.profiler.CStackAwareAbstractProfilerTest; +import com.datadoghq.profiler.Platform; +import com.datadoghq.profiler.junit.CStack; +import com.datadoghq.profiler.junit.RetryTest; +import org.junit.jupiter.api.TestTemplate; +import org.openjdk.jmc.common.item.IItem; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.item.IMemberAccessor; +import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; + +import org.junit.jupiter.params.provider.ValueSource; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.locks.LockSupport; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Integration tests for the virtual thread continuation unwind paths in walkVM. + * + * CPU-bound VT: never suspends, so all frames are thawed. The profiler detects + * cont_entry_return_pc as the return PC of the bottommost thawed frame and traverses + * enterSpecial to reach carrier frames. + * + * Blocking VT: parks/unparks repeatedly; when remounted with frozen frames still in the + * StackChunk, cont_returnBarrier is the return PC of the bottommost thawed frame. + * + * Skipped entirely on JDK < 21 via {@link #isPlatformSupported()}. + * + *

Negative-test mode

+ * Set {@code DDPROF_DISABLE_CONT_UNWIND=1} to skip both native unwind paths at runtime. + * With this flag the test will fail (carrier frames absent), confirming the fix is necessary. + */ +public class VirtualThreadWallClockTest extends CStackAwareAbstractProfilerTest { + + /** DCE sink for the CPU-bound spin loop. */ + private volatile long sink; + + public VirtualThreadWallClockTest(@CStack String cstack) { + super(cstack); + } + + @Override + protected boolean isPlatformSupported() { + return Platform.isJavaVersionAtLeast(21); + } + + @Override + protected String getProfilerCommand() { + return "wall=1ms,filter="; + } + + /** + * Starts a virtual thread using reflection so the class compiles with {@code --release 8}. + * Equivalent to {@code Thread.ofVirtual().start(task)}. + */ + private static Thread startVirtualThread(Runnable task) throws Exception { + // Thread.ofVirtual() -> Thread.Builder.OfVirtual + Method ofVirtual = Thread.class.getMethod("ofVirtual"); + Object builder = ofVirtual.invoke(null); + // Look up start(Runnable) via the public Thread.Builder interface to avoid + // IllegalAccessException on the internal ThreadBuilders$VirtualThreadBuilder class + Class builderInterface = Class.forName("java.lang.Thread$Builder"); + Method start = builderInterface.getMethod("start", Runnable.class); + return (Thread) start.invoke(builder, task); + } + + /** + * Asserts that carrier frames (ForkJoinWorkerThread) are visible in the stack traces, + * confirming that continuation unwinding is working correctly. + */ + private void assertCarrierFramesVisible(IItemCollection events) { + boolean carrierVisible = false; + for (IItemIterable samples : events) { + IMemberAccessor frameAccessor = + JdkAttributes.STACK_TRACE_STRING.getAccessor(samples.getType()); + if (frameAccessor == null) continue; + for (IItem sample : samples) { + String stackTrace = frameAccessor.getMember(sample); + if (stackTrace == null || !stackTrace.contains("VirtualThreadWallClockTest")) continue; + // Standard JDK VTs run on ForkJoinWorkerThread carriers. + // If the JVM ever changes the default carrier pool this check must be updated. + if (stackTrace.contains("ForkJoinWorkerThread")) { + carrierVisible = true; + break; + } + } + if (carrierVisible) break; + } + if (!carrierVisible) { + System.out.println("=== MISSING CARRIER — sample stack traces ==="); + int printed = 0; + outer: + for (IItemIterable dump : events) { + IMemberAccessor fa = + JdkAttributes.STACK_TRACE_STRING.getAccessor(dump.getType()); + if (fa == null) continue; + for (IItem sample : dump) { + String st = fa.getMember(sample); + if (st != null && st.contains("VirtualThreadWallClockTest")) { + System.out.println("--- vt sample " + (++printed) + " ---"); + System.out.println(st); + if (printed >= 5) break outer; + } + } + } + // Carrier-only samples: ForkJoinWorkerThread without VT frames + int carrierPrinted = 0; + System.out.println("=== CARRIER-ONLY samples (no VT frames) ==="); + outer2: + for (IItemIterable dump : events) { + IMemberAccessor fa = + JdkAttributes.STACK_TRACE_STRING.getAccessor(dump.getType()); + if (fa == null) continue; + for (IItem sample : dump) { + String st = fa.getMember(sample); + if (st != null && st.contains("ForkJoinWorkerThread") && !st.contains("VirtualThreadWallClockTest")) { + System.out.println("--- carrier " + (++carrierPrinted) + " ---"); + System.out.println(st); + if (carrierPrinted >= 3) break outer2; + } + } + } + if (carrierPrinted == 0) { + // No carrier samples at all — print first 3 arbitrary samples + System.out.println("=== No carrier samples — first 3 arbitrary samples ==="); + int anyPrinted = 0; + outer3: + for (IItemIterable dump : events) { + IMemberAccessor fa = + JdkAttributes.STACK_TRACE_STRING.getAccessor(dump.getType()); + if (fa == null) continue; + for (IItem sample : dump) { + String st = fa.getMember(sample); + if (st != null && !st.isEmpty()) { + System.out.println("--- any " + (++anyPrinted) + " ---"); + System.out.println(st); + if (anyPrinted >= 3) break outer3; + } + } + } + } + } + assertTrue(carrierVisible, + "No sample showed carrier-thread frames (ForkJoinWorkerThread) — continuation unwind may be broken"); + } + + /** + * CPU-bound virtual thread (all frames thawed). + * + * The VT runs a pure spin loop for ~2 seconds and never parks, so all frames are always + * thawed. The profiler detects cont_entry_return_pc and traverses enterSpecial to reach + * carrier frames. + */ + @RetryTest(5) + @TestTemplate + @ValueSource(strings = {"vm", "vmx", "fp", "dwarf"}) + public void samplesCarrierFramesFromCpuBoundVT(@CStack String cstack) throws Exception { + // Carrier-frame unwinding is only enabled for vmx (cstack=vm does not set carrier_frames). + // fp/dwarf use ASGCT and cannot cross the continuation boundary at all. + assumeTrue(cstack.equals("vmx"), + "carrier-frame unwind requires cstack=vmx"); + waitForProfilerReady(2_000); + Thread vt = startVirtualThread(() -> { + registerCurrentThreadForWallClockProfiling(); + long sum = 0; + long deadline = System.nanoTime() + 2_000_000_000L; + while (System.nanoTime() < deadline) { + sum ^= System.nanoTime(); + } + sink = sum; + }); + vt.join(); + stopProfiler(); + + Map countersA = profiler.getDebugCounters(); + System.out.println("=== COUNTERS CpuBound cstack=" + cstack + " ==="); + System.out.println(" enter_special_hit : " + countersA.getOrDefault("walkvm_enter_special_hit", 0L)); + System.out.println(" cont_barrier_hit : " + countersA.getOrDefault("walkvm_cont_barrier_hit", 0L)); + System.out.println(" cont_entry_null : " + countersA.getOrDefault("walkvm_cont_entry_null", 0L)); + System.out.println(" break_compiled : " + countersA.getOrDefault("walkvm_break_compiled", 0L)); + System.out.println(" hit_codeheap : " + countersA.getOrDefault("walkvm_hit_codeheap", 0L)); + System.out.println(" java_frame_ok : " + countersA.getOrDefault("walkvm_java_frame_ok", 0L)); + System.out.println(" no_vmthread : " + countersA.getOrDefault("walkvm_no_vmthread", 0L)); + System.out.println(" cached_not_java : " + countersA.getOrDefault("walkvm_cached_not_java", 0L)); + System.out.println(" break_interpreted : " + countersA.getOrDefault("walkvm_break_interpreted", 0L)); + System.out.println(" depth_zero : " + countersA.getOrDefault("walkvm_depth_zero", 0L)); + + verifyCStackSettings(); + + assertCarrierFramesVisible(verifyEvents("datadog.MethodSample")); + } + + /** + * Blocking virtual thread (frozen frames in StackChunk). + * + * The VT parks itself 100 times. The main thread sleeps 10ms between each unpark so the + * wall-clock sampler (1ms period) has time to fire while the VT is mounted with frozen + * frames still in the StackChunk (cont_returnBarrier as the return PC). + */ + @RetryTest(5) + @TestTemplate + @ValueSource(strings = {"vm", "vmx", "fp", "dwarf"}) + public void samplesCarrierFramesFromBlockingVT(@CStack String cstack) throws Exception { + // Carrier-frame unwinding is only enabled for vmx (cstack=vm does not set carrier_frames). + // fp/dwarf use ASGCT and cannot cross the continuation boundary at all. + assumeTrue(cstack.equals("vmx"), + "carrier-frame unwind requires cstack=vmx"); + // cont_returnBarrier detection is not yet verified on JDK 25+ where the stub + // may have changed; skip rather than fail until verified. + assumeTrue(!Platform.isJavaVersionAtLeast(25), + "cont_returnBarrier unwind not yet verified on JDK 25+"); + waitForProfilerReady(2_000); + final CountDownLatch started = new CountDownLatch(1); + Thread vt = startVirtualThread(() -> { + registerCurrentThreadForWallClockProfiling(); + started.countDown(); + for (int i = 0; i < 100; i++) { + LockSupport.park(); + // Do some work after unpark to give profiler a chance to sample + // while mounted with potentially frozen frames + long sum = 0; + for (int j = 0; j < 10000; j++) { + sum += System.nanoTime(); + } + sink = sum; + } + }); + started.await(); + for (int i = 0; i < 100; i++) { + Thread.sleep(10); // give wall-clock sampler time to fire during remount + LockSupport.unpark(vt); + } + vt.join(15_000); + assertFalse(vt.isAlive(), "Virtual thread did not complete within timeout"); + stopProfiler(); + + Map countersB = profiler.getDebugCounters(); + System.out.println("=== COUNTERS Blocking cstack=" + cstack + " ==="); + System.out.println(" enter_special_hit : " + countersB.getOrDefault("walkvm_enter_special_hit", 0L)); + System.out.println(" cont_barrier_hit : " + countersB.getOrDefault("walkvm_cont_barrier_hit", 0L)); + System.out.println(" cont_entry_null : " + countersB.getOrDefault("walkvm_cont_entry_null", 0L)); + System.out.println(" break_compiled : " + countersB.getOrDefault("walkvm_break_compiled", 0L)); + System.out.println(" hit_codeheap : " + countersB.getOrDefault("walkvm_hit_codeheap", 0L)); + System.out.println(" java_frame_ok : " + countersB.getOrDefault("walkvm_java_frame_ok", 0L)); + System.out.println(" no_vmthread : " + countersB.getOrDefault("walkvm_no_vmthread", 0L)); + System.out.println(" cached_not_java : " + countersB.getOrDefault("walkvm_cached_not_java", 0L)); + System.out.println(" break_interpreted : " + countersB.getOrDefault("walkvm_break_interpreted", 0L)); + System.out.println(" depth_zero : " + countersB.getOrDefault("walkvm_depth_zero", 0L)); + + verifyCStackSettings(); + + assertCarrierFramesVisible(verifyEvents("datadog.MethodSample")); + } +} From 4a794c4e69b452b67e7a913bf4df2d0827a53099 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 3 Apr 2026 13:26:49 +0200 Subject: [PATCH 5/5] chore(utils): update docker test scripts for JDK version matrix --- utils/release.sh | 123 +++++++++++++++++++++++++++++--------- utils/run-docker-tests.sh | 16 ++--- 2 files changed, 102 insertions(+), 37 deletions(-) diff --git a/utils/release.sh b/utils/release.sh index 9541a2d62..b2130c4fc 100755 --- a/utils/release.sh +++ b/utils/release.sh @@ -54,6 +54,91 @@ print_info() { echo -e "${BLUE}$1${NC}" } +# Read a single keypress (arrow keys, enter, q) from /dev/tty +read_key() { + local key + IFS= read -rsn1 key &1) + + if [ ${#branches[@]} -eq 0 ]; then + print_error "No release branches found matching release/X.Y._" >&2 + exit 1 + fi + + if [ ! -t 0 ]; then + print_error "Interactive mode requires a terminal" >&2 + print_error "Use --branch to specify a release branch" >&2 + exit 1 + fi + + local selected=0 + local total=${#branches[@]} + + display_branch_menu() { + clear >&2 + echo "" >&2 + echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════${NC}" >&2 + echo -e "${BLUE} Select Release Branch for Patch${NC}" >&2 + echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════${NC}" >&2 + echo "" >&2 + echo "Use ↑/↓ arrow keys to navigate, Enter to select, 'q' to quit" >&2 + echo "" >&2 + + for i in "${!branches[@]}"; do + if [ $i -eq $selected ]; then + echo -e "${GREEN}→ ${branches[$i]}${NC}" >&2 + else + echo -e " ${branches[$i]}" >&2 + fi + done + + echo "" >&2 + echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════${NC}" >&2 + } + + while true; do + display_branch_menu + key=$(read_key) + case $key in + up) + [ $selected -gt 0 ] && ((selected--)) + ;; + down) + [ $selected -lt $((total - 1)) ] && ((selected++)) + ;; + enter) + echo "${branches[$selected]}" + return 0 + ;; + quit) + echo "" >&2 + print_info "Selection cancelled" >&2 + exit 0 + ;; + esac + done +} + # Function to show interactive commit selector select_commit() { local branch=$1 @@ -102,28 +187,6 @@ select_commit() { echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════${NC}" >&2 } - # Read single keypress from /dev/tty - read_key() { - local key - IFS= read -rsn1 key /dev/null || true + echo "" + BRANCH=$(select_release_branch) + clear + print_info "Branch selected: $BRANCH" echo "" - echo "To create a patch release:" - echo " 1. Switch to a release branch: git checkout release/X.Y._" - echo " 2. Run: $0 patch" - exit 1 fi else if [ "$BRANCH" != "main" ]; then diff --git a/utils/run-docker-tests.sh b/utils/run-docker-tests.sh index 01e3f049a..6e4713ce4 100755 --- a/utils/run-docker-tests.sh +++ b/utils/run-docker-tests.sh @@ -80,14 +80,14 @@ get_glibc_jdk_url() { local arch=$2 case "$version-$arch" in - 8-x64) echo "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u432-b06/OpenJDK8U-jdk_x64_linux_hotspot_8u432b06.tar.gz" ;; - 8-aarch64) echo "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u432-b06/OpenJDK8U-jdk_aarch64_linux_hotspot_8u432b06.tar.gz" ;; - 11-x64) echo "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.25%2B9/OpenJDK11U-jdk_x64_linux_hotspot_11.0.25_9.tar.gz" ;; - 11-aarch64) echo "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.25%2B9/OpenJDK11U-jdk_aarch64_linux_hotspot_11.0.25_9.tar.gz" ;; - 17-x64) echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.13%2B11/OpenJDK17U-jdk_x64_linux_hotspot_17.0.13_11.tar.gz" ;; - 17-aarch64) echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.13%2B11/OpenJDK17U-jdk_aarch64_linux_hotspot_17.0.13_11.tar.gz" ;; - 21-x64) echo "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.5%2B11/OpenJDK21U-jdk_x64_linux_hotspot_21.0.5_11.tar.gz" ;; - 21-aarch64) echo "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.5%2B11/OpenJDK21U-jdk_aarch64_linux_hotspot_21.0.5_11.tar.gz" ;; + 8-x64) echo "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u462-b08/OpenJDK8U-jdk_x64_linux_hotspot_8u462b08.tar.gz" ;; + 8-aarch64) echo "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u462-b08/OpenJDK8U-jdk_aarch64_linux_hotspot_8u462b08.tar.gz" ;; + 11-x64) echo "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.28%2B6/OpenJDK11U-jdk_x64_linux_hotspot_11.0.28_6.tar.gz" ;; + 11-aarch64) echo "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.28%2B6/OpenJDK11U-jdk_aarch64_linux_hotspot_11.0.28_6.tar.gz" ;; + 17-x64) echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.16%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.16_8.tar.gz" ;; + 17-aarch64) echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.16%2B8/OpenJDK17U-jdk_aarch64_linux_hotspot_17.0.16_8.tar.gz" ;; + 21-x64) echo "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.8%2B9/OpenJDK21U-jdk_x64_linux_hotspot_21.0.8_9.tar.gz" ;; + 21-aarch64) echo "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.8%2B9/OpenJDK21U-jdk_aarch64_linux_hotspot_21.0.8_9.tar.gz" ;; 25-x64) echo "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz" ;; 25-aarch64) echo "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.2_10.tar.gz" ;; *) echo "" ;;