From 2d6d688aaaf0ea52d998abfd3f1ae1f0d747857f Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 7 Jul 2026 19:58:58 +0000 Subject: [PATCH 01/19] v0 --- ddprof-lib/src/main/cpp/javaApi.cpp | 40 +++++++---- ddprof-lib/src/main/cpp/jvmSupport.cpp | 6 +- ddprof-lib/src/main/cpp/profiler.cpp | 13 +++- ddprof-lib/src/main/cpp/profiler.h | 1 + ddprof-lib/src/main/cpp/threadLocalData.cpp | 76 ++++----------------- ddprof-lib/src/main/cpp/threadLocalData.h | 44 +++++++----- ddprof-lib/src/test/cpp/ddprof_ut.cpp | 3 +- 7 files changed, 83 insertions(+), 100 deletions(-) diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index c83a7bfb65..7cf296c03b 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -26,6 +26,7 @@ #include "engine.h" #include "hotspot/vmStructs.inline.h" #include "incbin.h" +#include "jvmSupport.h" #include "jvmThread.h" #include "os.h" #include "otel_process_ctx.h" @@ -68,6 +69,11 @@ class JniString { extern "C" DLLEXPORT jboolean JNICALL Java_com_datadoghq_profiler_JavaProfiler_init0(JNIEnv *env, jclass unused) { + Error error = Profiler::instance()->init(); + if (error) { + return JNI_FALSE; + } + // JavaVM* has already been stored when the native library was loaded so we can pass nullptr here return VM::initProfilerBridge(nullptr, true); } @@ -129,6 +135,19 @@ Java_com_datadoghq_profiler_JavaProfiler_getSamples(JNIEnv *env, return (jlong)Profiler::instance()->total_samples(); } +// Init or get current profiled thread. +// Calling thread's thread local may not be initialized due to race. +// Especially, during the early startup phase. +// This problem can be eliminated with native agent. +static ProfiledThread* initOrGetCurrentThread() { + ProfiledThread* current = ProfiledThread::current(); + if (current == nullptr) { + current = ProfiledThread::initCurrentThreadSignalSafe(); + } + return current; +} + + // some duplication between add and remove, though we want to avoid having an extra branch in the hot path // JavaCritical is faster JNI, but more restrictive - parameters and return value have to be @@ -137,7 +156,7 @@ Java_com_datadoghq_profiler_JavaProfiler_getSamples(JNIEnv *env, // still compatible in the event of signature changes in the future. extern "C" DLLEXPORT void JNICALL JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() { - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = initOrGetCurrentThread(); assert(current != nullptr); int tid = current->tid(); if (unlikely(tid < 0)) { @@ -167,7 +186,7 @@ JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() { extern "C" DLLEXPORT void JNICALL JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadRemove0() { - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = initOrGetCurrentThread(); assert(current != nullptr); int tid = current->tid(); if (unlikely(tid < 0)) { @@ -319,7 +338,7 @@ Java_com_datadoghq_profiler_JavaProfiler_recordQueueEnd0( extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(JNIEnv *env, jclass unused) { - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = initOrGetCurrentThread(); if (current == nullptr) { return; } @@ -338,9 +357,7 @@ extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_parkExit0( JNIEnv *env, jclass unused, jlong blocker, jlong unblockingSpanId) { ProfiledThread *current = ProfiledThread::current(); - if (current == nullptr) { - return; - } + assert(current != nullptr); u64 park_block_token = 0; if (!current->parkExit(park_block_token) || park_block_token == 0) { return; @@ -370,10 +387,9 @@ Java_com_datadoghq_profiler_JavaProfiler_blockEnter0( if (!decodeJavaBlockState(state, decoded)) { return 0; } - ProfiledThread *current = ProfiledThread::current(); - if (current == nullptr) { - return 0; - } + ProfiledThread *current = initOrGetCurrentThread(); + assert(current != nullptr); + ThreadFilter *tf = Profiler::instance()->threadFilter(); if (!tf->enabled()) { return 0; @@ -392,7 +408,7 @@ Java_com_datadoghq_profiler_JavaProfiler_blockExit0( if (block_token == 0) { return; } - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = initOrGetCurrentThread(); if (current == nullptr) { return; } @@ -685,7 +701,7 @@ Java_com_datadoghq_profiler_OTelContext_readProcessCtx0(JNIEnv *env, jclass unus extern "C" DLLEXPORT jobject JNICALL Java_com_datadoghq_profiler_JavaProfiler_initializeContextTLS0(JNIEnv* env, jclass unused, jlongArray metadata) { - ProfiledThread* thrd = ProfiledThread::current(); + ProfiledThread* thrd = initOrGetCurrentThread(); assert(thrd != nullptr); if (!thrd->isContextInitialized()) { diff --git a/ddprof-lib/src/main/cpp/jvmSupport.cpp b/ddprof-lib/src/main/cpp/jvmSupport.cpp index cff98c33b5..bf463e8b3e 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.cpp +++ b/ddprof-lib/src/main/cpp/jvmSupport.cpp @@ -32,12 +32,12 @@ bool JVMSupport::initialize() { return false; } - // Add ProfiledThread key checking here in next PR - return true; + // Check ProfiledThread key, it is critical for storing per-thread metadata + return ProfiledThread::isThreadKeyValid(); } bool JVMSupport::isInitialized() { - return JVMThread::isInitialized(); + return JVMThread::isInitialized() && ProfiledThread::isThreadKeyValid(); } JVMSupport::JMethodIDLoadStats JVMSupport::getLoadState() { diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index dde305272e..5c9da37762 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1235,6 +1235,17 @@ Error Profiler::checkState() { return Error::OK; } +Error Profiler::init() { + MutexLocker ml(_state_lock); + Error error = checkState(); + if (error) { + return error; + } + + ProfiledThread::initCurrentThread(); + return Error::OK; +} + Error Profiler::start(Arguments &args, bool reset) { MutexLocker ml(_state_lock); Error error = checkState(); @@ -1356,7 +1367,7 @@ Error Profiler::start(Arguments &args, bool reset) { // Minor optim: Register the current thread (start thread won't be called) if (_thread_filter.enabled()) { _thread_filter.clearActive(); - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = ProfiledThread::initCurrentThread(); assert(current != nullptr); int slot_id = current->filterSlotId(); if (slot_id < 0) { diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 1e1b2825ec..7dbaa763fb 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -285,6 +285,7 @@ class alignas(alignof(SpinLock)) Profiler { return __atomic_load_n(&_epoch, __ATOMIC_RELAXED); } + Error init(); Error run(Arguments &args); Error runInternal(Arguments &args, std::ostream &out); Error restart(Arguments &args); diff --git a/ddprof-lib/src/main/cpp/threadLocalData.cpp b/ddprof-lib/src/main/cpp/threadLocalData.cpp index 1843514bd2..ce2a846726 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.cpp +++ b/ddprof-lib/src/main/cpp/threadLocalData.cpp @@ -11,57 +11,26 @@ #include #include -pthread_key_t ProfiledThread::_tls_key; -bool ProfiledThread::_tls_key_initialized = false; -void ProfiledThread::initTLSKey() { - static pthread_once_t tls_initialized = PTHREAD_ONCE_INIT; - pthread_once(&tls_initialized, doInitTLSKey); -} - -void ProfiledThread::doInitTLSKey() { - pthread_key_create(&_tls_key, freeKey); - // Must be set AFTER pthread_key_create so signal handlers see a valid key. - // Store-release pairs with the acquire loads in currentSignalSafe() and release() - // to prevent hardware load-load reordering on weakly-ordered architectures (aarch64): - // a plain volatile write is not sufficient there. - __atomic_store_n(&_tls_key_initialized, true, __ATOMIC_RELEASE); -} +ThreadLocal ProfiledThread::_current_thread; -inline void ProfiledThread::freeKey(void *key) { - ProfiledThread *tls_ref = (ProfiledThread *)(key); - if (tls_ref != NULL) { - SignalBlocker blocker; - delete tls_ref; +ProfiledThread* ProfiledThread::initCurrentThread() { + ProfiledThread* tls = current(); + if (tls == nullptr) { + int tid = OS::threadId(); + tls = ProfiledThread::forTid(tid); + _current_thread.set(tls); } + return tls; } -void ProfiledThread::initCurrentThread() { - // JVMTI callback path - does NOT use buffer - // Allocate dedicated ProfiledThread for Java threads (not from buffer) - // This MUST happen here to prevent lazy allocation in signal handler - initTLSKey(); - - if (pthread_getspecific(_tls_key) != NULL) { - return; // Already initialized - } - - int tid = OS::threadId(); - ProfiledThread *tls = ProfiledThread::forTid(tid); - pthread_setspecific(_tls_key, (const void *)tls); +ProfiledThread* ProfiledThread::initCurrentThreadSignalSafe() { + SignalBlocker blocker; + return initCurrentThread(); } void ProfiledThread::release() { - if (!__atomic_load_n(&_tls_key_initialized, __ATOMIC_ACQUIRE)) { - return; - } - pthread_key_t key = _tls_key; - ProfiledThread *tls = (ProfiledThread *)pthread_getspecific(key); - if (tls != NULL) { - SignalBlocker blocker; - pthread_setspecific(key, NULL); - delete tls; - } + _current_thread.clear(); } int ProfiledThread::currentTid() { @@ -72,27 +41,6 @@ int ProfiledThread::currentTid() { return OS::threadId(); } -ProfiledThread *ProfiledThread::current() { - initTLSKey(); - - ProfiledThread *tls = (ProfiledThread *)pthread_getspecific(_tls_key); - if (tls == NULL) { - // Lazy allocation - safe since current() is never called from signal handlers - int tid = OS::threadId(); - tls = ProfiledThread::forTid(tid); - pthread_setspecific(_tls_key, (const void *)tls); - } - return tls; -} - -ProfiledThread *ProfiledThread::currentSignalSafe() { - // Signal-safe: never allocate, just return existing TLS or null. - // Use _tls_key_initialized instead of key != 0 because pthread_key_create - // can legitimately return key 0 (common on musl where keys start at 0). - return __atomic_load_n(&_tls_key_initialized, __ATOMIC_ACQUIRE) ? (ProfiledThread *)pthread_getspecific(_tls_key) : nullptr; -} - - Context ProfiledThread::snapshotContext(size_t numAttrs) { Context ctx = {}; u64 span_id = 0, root_span_id = 0; diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index e8b9a30c46..c886229357 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -9,6 +9,7 @@ #include "context.h" #include "otel_context.h" #include "os.h" +#include "threadLocal.h" #include "threadState.h" #include "unwindStats.h" #include @@ -53,12 +54,7 @@ class ProfiledThread : public ThreadLocalData { // Even with a 5-level cap we can still encounter highly recursive signal handlers. static constexpr u32 CRASH_HANDLER_NESTING_LIMIT = 5; private: - static pthread_key_t _tls_key; - static bool _tls_key_initialized; - - static void initTLSKey(); - static void doInitTLSKey(); - static inline void freeKey(void *key); + static ThreadLocal _current_thread; // longjmp buffer. Used by hotspot only at this moment. // Published in walkVM() and consumed in checkFault() from an asynchronous @@ -108,32 +104,44 @@ class ProfiledThread : public ThreadLocalData { virtual ~ProfiledThread() { } public: static ProfiledThread *forTid(int tid) { return new ProfiledThread(tid); } + static bool isThreadKeyValid() { + return _current_thread.isKeyValid(); + } - static void initCurrentThread(); - static void release(); #ifdef UNIT_TEST // Simulates the moment inside release() after pthread_setspecific(NULL) but // before delete — the race window the clearCurrentThreadTLS fix covers. // Returns the detached pointer so the caller can delete it after assertions. static ProfiledThread* clearCurrentThreadTLS() { - if (__atomic_load_n(&_tls_key_initialized, __ATOMIC_ACQUIRE)) { - ProfiledThread *pt = (ProfiledThread *)pthread_getspecific(_tls_key); - pthread_setspecific(_tls_key, nullptr); - return pt; - } - return nullptr; + assert(isThreadKeyValid() && "Should not reach here - profiling should have been disabled"); + ProfiledThread* pt = _current_thread.get(); + _current_thread.clear(); + return pt; } // Deletes a ProfiledThread returned by clearCurrentThreadTLS(). // Needed because the destructor is private. static void deleteForTest(ProfiledThread *pt) { delete pt; } #endif + // initCurrentThread() and release() are not async-signal-safe: + // must call outside of a signal handler with signal blocked + static ProfiledThread* initCurrentThread(); + static void release(); - static ProfiledThread *current(); - static ProfiledThread *currentSignalSafe(); // Signal-safe version that never allocates - static int currentTid(); + // Async-signal-safe version + static ProfiledThread* initCurrentThreadSignalSafe(); - inline int tid() { return _tid; } + // This call is async-signal-safe + static inline ProfiledThread *current() { + assert(isThreadKeyValid() && "Should not reach here - profiling should have been disabled"); + return _current_thread.get(); + } + + static inline ProfiledThread *currentSignalSafe() { // Signal-safe version that never allocates + return current(); + } + static int currentTid(); + inline int tid() { return _tid; } inline u64 noteCPUSample(u32 recording_epoch) { _recording_epoch = recording_epoch; return ++_cpu_epoch; diff --git a/ddprof-lib/src/test/cpp/ddprof_ut.cpp b/ddprof-lib/src/test/cpp/ddprof_ut.cpp index 733bbb4433..c3926d4a44 100644 --- a/ddprof-lib/src/test/cpp/ddprof_ut.cpp +++ b/ddprof-lib/src/test/cpp/ddprof_ut.cpp @@ -382,8 +382,7 @@ static DdprofGlobalSetup ddprof_global_setup; if (pid == 0) { // ---- child process (fork isolates TLS from other tests) ---- - ProfiledThread::initCurrentThread(); - ProfiledThread* pt = ProfiledThread::currentSignalSafe(); + ProfiledThread* pt = ProfiledThread::initCurrentThread(); if (pt == nullptr) _exit(2); // Baseline: entering critical section works. From f7e32556b7c72ba1dd54753786253d9b739f8b10 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 7 Jul 2026 23:03:35 +0000 Subject: [PATCH 02/19] AI reviews --- ddprof-lib/src/main/cpp/javaApi.cpp | 12 ++++++++---- ddprof-lib/src/main/cpp/threadLocalData.cpp | 6 +++++- ddprof-lib/src/main/cpp/threadLocalData.h | 3 ++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index 7cf296c03b..7cc91f75cd 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -356,8 +356,11 @@ Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(JNIEnv *env, jclass unused) extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_parkExit0( JNIEnv *env, jclass unused, jlong blocker, jlong unblockingSpanId) { - ProfiledThread *current = ProfiledThread::current(); - assert(current != nullptr); + ProfiledThread *current = initOrGetCurrentThread(); + if (current == nullptr) { + return; + } + u64 park_block_token = 0; if (!current->parkExit(park_block_token) || park_block_token == 0) { return; @@ -388,8 +391,9 @@ Java_com_datadoghq_profiler_JavaProfiler_blockEnter0( return 0; } ProfiledThread *current = initOrGetCurrentThread(); - assert(current != nullptr); - + if (current == nullptr) { + return 0; + } ThreadFilter *tf = Profiler::instance()->threadFilter(); if (!tf->enabled()) { return 0; diff --git a/ddprof-lib/src/main/cpp/threadLocalData.cpp b/ddprof-lib/src/main/cpp/threadLocalData.cpp index ce2a846726..b6288148cd 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.cpp +++ b/ddprof-lib/src/main/cpp/threadLocalData.cpp @@ -30,7 +30,11 @@ ProfiledThread* ProfiledThread::initCurrentThreadSignalSafe() { } void ProfiledThread::release() { - _current_thread.clear(); + ProfiledThread* pt = _current_thread.get(); + if (pt != nullptr) { + _current_thread.clear(); + delete pt; + } } int ProfiledThread::currentTid() { diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index c886229357..513a828824 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -127,7 +127,8 @@ class ProfiledThread : public ThreadLocalData { static ProfiledThread* initCurrentThread(); static void release(); - // Async-signal-safe version + // This version blocks signals, so that initialization cannot + // be interrupted by signals static ProfiledThread* initCurrentThreadSignalSafe(); // This call is async-signal-safe From 1793aea0ed5b13249e6032871a618990170f90b3 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 8 Jul 2026 15:10:07 +0200 Subject: [PATCH 03/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/javaApi.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index 7cc91f75cd..fd3a842227 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -71,6 +71,7 @@ extern "C" DLLEXPORT jboolean JNICALL Java_com_datadoghq_profiler_JavaProfiler_init0(JNIEnv *env, jclass unused) { Error error = Profiler::instance()->init(); if (error) { + throwNew(env, "java/lang/IllegalStateException", error.message()); return JNI_FALSE; } From 4414df3265926939f99c2d971ab0f887b1a1ef30 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 8 Jul 2026 17:28:29 +0200 Subject: [PATCH 04/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/threadLocalData.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ddprof-lib/src/main/cpp/threadLocalData.cpp b/ddprof-lib/src/main/cpp/threadLocalData.cpp index b6288148cd..bcf6131a8c 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.cpp +++ b/ddprof-lib/src/main/cpp/threadLocalData.cpp @@ -15,7 +15,10 @@ ThreadLocal ProfiledThread::_current_thread; ProfiledThread* ProfiledThread::initCurrentThread() { - ProfiledThread* tls = current(); + if (!isThreadKeyValid()) { + return nullptr; + } + ProfiledThread* tls = _current_thread.get(); if (tls == nullptr) { int tid = OS::threadId(); tls = ProfiledThread::forTid(tid); From e74e4b3d7f2b4fff579930e2144084bcb2fa8601 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 8 Jul 2026 17:31:33 +0200 Subject: [PATCH 05/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/threadLocalData.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 513a828824..fd709f13ea 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -131,9 +131,11 @@ class ProfiledThread : public ThreadLocalData { // be interrupted by signals static ProfiledThread* initCurrentThreadSignalSafe(); - // This call is async-signal-safe + // Signal-handler friendly (no allocation): returns existing TLS or nullptr. static inline ProfiledThread *current() { - assert(isThreadKeyValid() && "Should not reach here - profiling should have been disabled"); + if (!isThreadKeyValid()) { + return nullptr; + } return _current_thread.get(); } From e1ed4cfcb29a98fd9a6e0793d9dfa95ef2f13d14 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 8 Jul 2026 15:53:48 +0000 Subject: [PATCH 06/19] v1 --- .../cpp/hotspot/hotspotStackFrame_x64.cpp | 1 + ddprof-lib/src/main/cpp/javaApi.cpp | 28 ++++++++++++------- .../src/main/cpp/libraryPatcher_linux.cpp | 15 ++++++---- ddprof-lib/src/main/cpp/threadLocalData.cpp | 13 +++++---- ddprof-lib/src/main/cpp/threadLocalData.h | 3 +- ddprof-lib/src/test/cpp/jvmSupport_ut.cpp | 15 ++++++++++ .../test/cpp/thread_teardown_safety_ut.cpp | 4 +-- 7 files changed, 53 insertions(+), 26 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame_x64.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame_x64.cpp index 24ab30b112..dc87459619 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame_x64.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame_x64.cpp @@ -8,6 +8,7 @@ #include #include "hotspot/hotspotStackFrame.h" +#include "hotspot/vmStructs.inline.h" __attribute__((no_sanitize("address"))) bool HotspotStackFrame::unwindStub(instruction_t* entry, const char* name, uintptr_t& pc, uintptr_t& sp, uintptr_t& fp) { instruction_t* ip = (instruction_t*)pc; diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index 7cc91f75cd..6dcd8fecb2 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -138,7 +138,9 @@ Java_com_datadoghq_profiler_JavaProfiler_getSamples(JNIEnv *env, // Init or get current profiled thread. // Calling thread's thread local may not be initialized due to race. // Especially, during the early startup phase. -// This problem can be eliminated with native agent. +// This call is expensive, should be avoided on hot paths. +// Note: the racy can be avoided with native agent, remove this method +// once converted to native agent. static ProfiledThread* initOrGetCurrentThread() { ProfiledThread* current = ProfiledThread::current(); if (current == nullptr) { @@ -147,7 +149,6 @@ static ProfiledThread* initOrGetCurrentThread() { return current; } - // some duplication between add and remove, though we want to avoid having an extra branch in the hot path // JavaCritical is faster JNI, but more restrictive - parameters and return value have to be @@ -156,8 +157,11 @@ static ProfiledThread* initOrGetCurrentThread() { // still compatible in the event of signature changes in the future. extern "C" DLLEXPORT void JNICALL JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() { - ProfiledThread *current = initOrGetCurrentThread(); - assert(current != nullptr); + ProfiledThread *current = ProfiledThread::current(); + if(current == nullptr) { + return; + } + int tid = current->tid(); if (unlikely(tid < 0)) { return; @@ -186,8 +190,11 @@ JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() { extern "C" DLLEXPORT void JNICALL JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadRemove0() { - ProfiledThread *current = initOrGetCurrentThread(); - assert(current != nullptr); + ProfiledThread *current = ProfiledThread::current(); + if(current == nullptr) { + return; + } + int tid = current->tid(); if (unlikely(tid < 0)) { return; @@ -338,7 +345,7 @@ Java_com_datadoghq_profiler_JavaProfiler_recordQueueEnd0( extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(JNIEnv *env, jclass unused) { - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::current(); if (current == nullptr) { return; } @@ -356,7 +363,7 @@ Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(JNIEnv *env, jclass unused) extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_parkExit0( JNIEnv *env, jclass unused, jlong blocker, jlong unblockingSpanId) { - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::current(); if (current == nullptr) { return; } @@ -390,7 +397,7 @@ Java_com_datadoghq_profiler_JavaProfiler_blockEnter0( if (!decodeJavaBlockState(state, decoded)) { return 0; } - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::current(); if (current == nullptr) { return 0; } @@ -412,7 +419,7 @@ Java_com_datadoghq_profiler_JavaProfiler_blockExit0( if (block_token == 0) { return; } - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::current(); if (current == nullptr) { return; } @@ -705,6 +712,7 @@ Java_com_datadoghq_profiler_OTelContext_readProcessCtx0(JNIEnv *env, jclass unus extern "C" DLLEXPORT jobject JNICALL Java_com_datadoghq_profiler_JavaProfiler_initializeContextTLS0(JNIEnv* env, jclass unused, jlongArray metadata) { + // Assume this call is not on hot path ProfiledThread* thrd = initOrGetCurrentThread(); assert(thrd != nullptr); diff --git a/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp b/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp index 0b14bd1449..bbf65e77a9 100644 --- a/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp +++ b/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp @@ -69,8 +69,9 @@ class RoutineInfo { // SignalBlocker's sigset_t does not appear in the caller's stack frame on // musl/aarch64 where the deopt blob may corrupt the wrapper's stack guard. __attribute__((noinline)) -static void unregister_and_release(int tid) { +static void unregister_and_release() { SignalBlocker blocker; + int tid = ProfiledThread::currentTid(); Profiler::unregisterThread(tid); ProfiledThread::release(); } @@ -83,7 +84,7 @@ static void unregister_and_release(int tid) { // appear in the caller's frame on platforms with stack-protector canaries. __attribute__((noinline)) static void cleanup_unregister(void*) { - unregister_and_release(ProfiledThread::currentTid()); + unregister_and_release(); } // Thread-cleanup wrapper that avoids the static-libgcc / forced-unwind crash. @@ -254,8 +255,8 @@ static void delete_routine_info(RoutineInfo* thr) { __attribute__((noinline)) static void init_tls_and_register() { SignalBlocker blocker; - ProfiledThread::initCurrentThread(); - if (ProfiledThread *pt = ProfiledThread::currentSignalSafe()) { + ProfiledThread* pt = ProfiledThread::initCurrentThread(); + if (pt != nullptr) { pt->startInitWindow(); } Profiler::registerThread(ProfiledThread::currentTid()); @@ -371,8 +372,10 @@ static void* start_routine_wrapper(void* args) { routine = thr->routine(); params = thr->args(); delete thr; - ProfiledThread::initCurrentThread(); - ProfiledThread::currentSignalSafe()->startInitWindow(); + ProfiledThread* pt = ProfiledThread::initCurrentThread(); + if (pt != nullptr) { + pt->startInitWindow(); + } Profiler::registerThread(ProfiledThread::currentTid()); } // Use POSIX cleanup instead of C++ RAII to handle pthread_exit(): see run_with_cleanup. diff --git a/ddprof-lib/src/main/cpp/threadLocalData.cpp b/ddprof-lib/src/main/cpp/threadLocalData.cpp index b6288148cd..5039205247 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.cpp +++ b/ddprof-lib/src/main/cpp/threadLocalData.cpp @@ -12,7 +12,7 @@ #include -ThreadLocal ProfiledThread::_current_thread; +ThreadLocal ProfiledThread::_current_thread; ProfiledThread* ProfiledThread::initCurrentThread() { ProfiledThread* tls = current(); @@ -29,12 +29,13 @@ ProfiledThread* ProfiledThread::initCurrentThreadSignalSafe() { return initCurrentThread(); } +void ProfiledThread::freeValue(void* value) { + ProfiledThread* pt = reinterpret_cast(value); + delete pt; +} + void ProfiledThread::release() { - ProfiledThread* pt = _current_thread.get(); - if (pt != nullptr) { - _current_thread.clear(); - delete pt; - } + _current_thread.clear(); } int ProfiledThread::currentTid() { diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 513a828824..7d9eeed663 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -54,8 +54,9 @@ class ProfiledThread : public ThreadLocalData { // Even with a 5-level cap we can still encounter highly recursive signal handlers. static constexpr u32 CRASH_HANDLER_NESTING_LIMIT = 5; private: - static ThreadLocal _current_thread; + static void freeValue(void* value); + static ThreadLocal _current_thread; // longjmp buffer. Used by hotspot only at this moment. // Published in walkVM() and consumed in checkFault() from an asynchronous // SEGV-handler context on the same thread; atomic makes the publish/observe diff --git a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp index 3a1af6dade..6557567ebc 100644 --- a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp +++ b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp @@ -145,6 +145,21 @@ TEST_F(JvmSupportInitFailureTest, CheckReturnsErrorAndLatchesErrorState) { EXPECT_EQ(ERROR, ProfilerTestAccessor::getState(p)); } +// Profiler::init() (invoked from JavaProfiler.init0() right after the native +// library loads) shares checkState()'s NEW-state JVMSupport::initialize() +// gate. It must surface the same failure and latch ERROR instead of +// proceeding to ProfiledThread::initCurrentThread(). +TEST_F(JvmSupportInitFailureTest, InitReturnsErrorAndLatchesErrorState) { + Profiler* p = Profiler::instance(); + + Error error = p->init(); + bool has_error = (bool)error; + + EXPECT_TRUE(has_error); + EXPECT_STREQ("Profiler encountered fatal error", error.message()); + EXPECT_EQ(ERROR, ProfilerTestAccessor::getState(p)); +} + // Once JVMSupport initialization has failed once, the profiler is // permanently disabled: checkState() must short-circuit on the ERROR state // on every later call without retrying JVMSupport::initialize() (no mock diff --git a/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp b/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp index 292f340758..6f775c6438 100644 --- a/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp +++ b/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp @@ -133,9 +133,7 @@ static void *t02_body(void *) { // release() with TLS already null must not double-free. ProfiledThread::release(); - // Complete the simulated teardown: delete the object (mirrors what freeKey - // would do). Destructor is private so we need the test helper. - ProfiledThread::deleteForTest(detached); + EXPECT_EQ(nullptr, ProfiledThread::current()); return nullptr; } From dfbe74861c88fc42edb3b08c10fd7e8275e024ea Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 8 Jul 2026 18:46:41 +0000 Subject: [PATCH 07/19] Fix test --- ddprof-lib/src/main/cpp/threadLocalData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 5a18c10a50..dfac3bc743 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -116,7 +116,7 @@ class ProfiledThread : public ThreadLocalData { static ProfiledThread* clearCurrentThreadTLS() { assert(isThreadKeyValid() && "Should not reach here - profiling should have been disabled"); ProfiledThread* pt = _current_thread.get(); - _current_thread.clear(); + _current_thread.set(nullptr); return pt; } // Deletes a ProfiledThread returned by clearCurrentThreadTLS(). From 7b9ac45de0898f242f09f754d3401c46107fb979 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 8 Jul 2026 20:03:22 +0000 Subject: [PATCH 08/19] Remove ProfiledThread::currentSignalSafe() --- ddprof-lib/src/main/cpp/context_api.cpp | 4 +-- ddprof-lib/src/main/cpp/ctimer_linux.cpp | 4 +-- ddprof-lib/src/main/cpp/flightRecorder.cpp | 2 +- ddprof-lib/src/main/cpp/guards.cpp | 14 ++++----- .../cpp/hotspot/hotspotStackFrame_x64.cpp | 3 -- .../src/main/cpp/hotspot/hotspotSupport.cpp | 6 ++-- ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp | 2 +- .../src/main/cpp/hotspot/vmStructs.inline.h | 2 +- ddprof-lib/src/main/cpp/itimer.cpp | 4 +-- ddprof-lib/src/main/cpp/jvmSupport.cpp | 2 +- .../src/main/cpp/libraryPatcher_linux.cpp | 2 +- ddprof-lib/src/main/cpp/perfEvents_linux.cpp | 2 +- ddprof-lib/src/main/cpp/profiler.cpp | 6 ++-- ddprof-lib/src/main/cpp/refCountGuard.cpp | 2 +- ddprof-lib/src/main/cpp/signalSafety.h | 4 +-- ddprof-lib/src/main/cpp/threadLocalData.h | 4 --- ddprof-lib/src/main/cpp/wallClock.cpp | 4 +-- ddprof-lib/src/test/cpp/ddprof_ut.cpp | 2 +- .../test/cpp/hotspot_crash_protection_ut.cpp | 6 ++-- .../test/cpp/thread_teardown_safety_ut.cpp | 30 +++++++++---------- 20 files changed, 49 insertions(+), 56 deletions(-) diff --git a/ddprof-lib/src/main/cpp/context_api.cpp b/ddprof-lib/src/main/cpp/context_api.cpp index 37122fe0c8..082131a921 100644 --- a/ddprof-lib/src/main/cpp/context_api.cpp +++ b/ddprof-lib/src/main/cpp/context_api.cpp @@ -41,7 +41,7 @@ void ContextApi::initializeContextTLS(ProfiledThread* thrd) { } bool ContextApi::get(u64& span_id, u64& root_span_id) { - ProfiledThread* thrd = ProfiledThread::currentSignalSafe(); + ProfiledThread* thrd = ProfiledThread::current(); if (thrd == nullptr || !thrd->isContextInitialized()) { return false; } @@ -59,7 +59,7 @@ bool ContextApi::get(u64& span_id, u64& root_span_id) { } Context ContextApi::snapshot() { - ProfiledThread* thrd = ProfiledThread::currentSignalSafe(); + ProfiledThread* thrd = ProfiledThread::current(); if (thrd == nullptr) { return {}; } diff --git a/ddprof-lib/src/main/cpp/ctimer_linux.cpp b/ddprof-lib/src/main/cpp/ctimer_linux.cpp index ae035a426c..11502c4883 100644 --- a/ddprof-lib/src/main/cpp/ctimer_linux.cpp +++ b/ddprof-lib/src/main/cpp/ctimer_linux.cpp @@ -225,7 +225,7 @@ void CTimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) { return; } int tid = 0; - ProfiledThread *current = ProfiledThread::currentSignalSafe(); + ProfiledThread *current = ProfiledThread::current(); assert(current == nullptr || !current->isDeepCrashHandler()); if (current != nullptr && JVMThread::current() == nullptr && current->inInitWindow()) { @@ -281,7 +281,7 @@ void CTimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) { return; } int tid = 0; - ProfiledThread *current = ProfiledThread::currentSignalSafe(); + ProfiledThread *current = ProfiledThread::current(); assert(current == nullptr || !current->isDeepCrashHandler()); // Guard against the race window between Profiler::registerThread() and // thread_native_entry setting JVM TLS (PROF-13072): skip at most one signal diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index 1bd4ec1bcd..e3857a53e4 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -1771,7 +1771,7 @@ void Recording::writeCurrentContext(Buffer *buf) { buf->putVar64(rootSpanId); size_t numAttrs = Profiler::instance()->numContextAttributes(); - ProfiledThread* thrd = hasContext ? ProfiledThread::currentSignalSafe() : nullptr; + ProfiledThread* thrd = hasContext ? ProfiledThread::current() : nullptr; for (size_t i = 0; i < numAttrs; i++) { buf->putVar32(thrd != nullptr ? thrd->getOtelTagEncoding(i) : 0); } diff --git a/ddprof-lib/src/main/cpp/guards.cpp b/ddprof-lib/src/main/cpp/guards.cpp index 45f05a59d8..9905182e9a 100644 --- a/ddprof-lib/src/main/cpp/guards.cpp +++ b/ddprof-lib/src/main/cpp/guards.cpp @@ -24,12 +24,12 @@ // rejected because of the static TLS surplus on Graal). int getInSignalDepth() { - ProfiledThread *pt = ProfiledThread::currentSignalSafe(); + ProfiledThread *pt = ProfiledThread::current(); return pt != nullptr ? static_cast(pt->signalDepth()) : 0; } bool isInTrackedSignalContext() { - ProfiledThread *pt = ProfiledThread::currentSignalSafe(); + ProfiledThread *pt = ProfiledThread::current(); // null ProfiledThread = no thread context; the SignalHandlerScope // never ran, so we have no positive evidence of a signal frame. // See header comment for the rationale of returning false here. @@ -37,7 +37,7 @@ bool isInTrackedSignalContext() { } SignalHandlerScope::SignalHandlerScope() : _active(true) { - ProfiledThread *pt = ProfiledThread::currentSignalSafe(); + ProfiledThread *pt = ProfiledThread::current(); if (pt != nullptr) { pt->enterSignalScope(); } else { @@ -49,7 +49,7 @@ SignalHandlerScope::SignalHandlerScope() : _active(true) { SignalHandlerScope::~SignalHandlerScope() { if (!_active) return; - ProfiledThread *pt = ProfiledThread::currentSignalSafe(); + ProfiledThread *pt = ProfiledThread::current(); if (pt != nullptr) { pt->exitSignalScope(); } @@ -57,7 +57,7 @@ SignalHandlerScope::~SignalHandlerScope() { void SignalHandlerScope::release() { if (!_active) return; - ProfiledThread *pt = ProfiledThread::currentSignalSafe(); + ProfiledThread *pt = ProfiledThread::current(); if (pt != nullptr) { pt->exitSignalScope(); } @@ -65,7 +65,7 @@ void SignalHandlerScope::release() { } void signalHandlerUnwindAfterLongjmp() { - ProfiledThread *pt = ProfiledThread::currentSignalSafe(); + ProfiledThread *pt = ProfiledThread::current(); if (pt != nullptr) { pt->exitSignalScope(); } @@ -75,7 +75,7 @@ void signalHandlerUnwindAfterLongjmp() { uint64_t CriticalSection::_fallback_bitmap[CriticalSection::FALLBACK_BITMAP_WORDS] = {}; CriticalSection::CriticalSection() : _entered(false), _using_fallback(false), _word_index(0), _bit_mask(0), _thread_ptr(nullptr) { - _thread_ptr = ProfiledThread::currentSignalSafe(); + _thread_ptr = ProfiledThread::current(); if (_thread_ptr != nullptr) { // Primary path: Use ProfiledThread storage (fast and memory-efficient) _entered = _thread_ptr->tryEnterCriticalSection(); diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame_x64.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame_x64.cpp index 34ae61e8b6..dad23b0dac 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame_x64.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame_x64.cpp @@ -8,11 +8,8 @@ #include #include "hotspot/hotspotStackFrame.h" -<<<<<<< HEAD -======= // See the note in hotspotStackFrame_aarch64.cpp: the VMNMethod accessors need vmStructs.inline.h // for crashProtectionActive() / cast_to() so assertion-enabled builds link. ->>>>>>> main #include "hotspot/vmStructs.inline.h" __attribute__((no_sanitize("address"))) bool HotspotStackFrame::unwindStub(instruction_t* entry, const char* name, uintptr_t& pc, uintptr_t& sp, uintptr_t& fp) { diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index d0b6c48eeb..ab77c8c4b8 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -232,7 +232,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex // VMStructs is only available for hotspot JVM assert(VM::isHotspot()); - ProfiledThread* prof_thread = ProfiledThread::currentSignalSafe(); + ProfiledThread* prof_thread = ProfiledThread::current(); if (prof_thread == nullptr) { Counters::increment(SAMPLES_DROPPED_THREAD_LOCAL); return 0; @@ -1198,7 +1198,7 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { if (cstack >= CSTACK_VM) { java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); } else { - AsyncSampleMutex mutex(ProfiledThread::currentSignalSafe()); + AsyncSampleMutex mutex(ProfiledThread::current()); if (mutex.acquired()) { java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { @@ -1223,7 +1223,7 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); } else { // Async events - AsyncSampleMutex mutex(ProfiledThread::currentSignalSafe()); + AsyncSampleMutex mutex(ProfiledThread::current()); if (mutex.acquired()) { java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp index e80b4151ed..d5453ce917 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp @@ -684,7 +684,7 @@ bool VMThread::isJavaThread(VMThread* vm_thread) { // JVMTI ThreadStart callback may have set the flag, which is reliable. // Or we may already compute and cache it, so use it instead. - ProfiledThread *prof_thread = ProfiledThread::currentSignalSafe(); + ProfiledThread *prof_thread = ProfiledThread::current(); if (prof_thread != nullptr) { ProfiledThread::ThreadType type = prof_thread->threadType(); if (type != ProfiledThread::ThreadType::TYPE_UNKNOWN) { diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.inline.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.inline.h index 910717ddfa..2b7edb3250 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.inline.h +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.inline.h @@ -13,7 +13,7 @@ #include "threadLocalData.h" inline bool crashProtectionActive() { - ProfiledThread* pt = ProfiledThread::currentSignalSafe(); + ProfiledThread* pt = ProfiledThread::current(); if (pt != nullptr) { return pt->isProtected(); } diff --git a/ddprof-lib/src/main/cpp/itimer.cpp b/ddprof-lib/src/main/cpp/itimer.cpp index 8f1d0a8f9e..0c1a134df3 100644 --- a/ddprof-lib/src/main/cpp/itimer.cpp +++ b/ddprof-lib/src/main/cpp/itimer.cpp @@ -49,7 +49,7 @@ void ITimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) { return; // Another critical section is active, defer profiling } int tid = 0; - ProfiledThread *current = ProfiledThread::currentSignalSafe(); + ProfiledThread *current = ProfiledThread::current(); if (current != NULL) { current->noteCPUSample(Profiler::instance()->recordingEpoch()); tid = current->tid(); @@ -116,7 +116,7 @@ void ITimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) { errno = saved_errno; return; } - ProfiledThread *current = ProfiledThread::currentSignalSafe(); + ProfiledThread *current = ProfiledThread::current(); if (current != nullptr && JVMThread::current() == nullptr && current->inInitWindow()) { current->tickInitWindow(); diff --git a/ddprof-lib/src/main/cpp/jvmSupport.cpp b/ddprof-lib/src/main/cpp/jvmSupport.cpp index bf463e8b3e..45c10332d4 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.cpp +++ b/ddprof-lib/src/main/cpp/jvmSupport.cpp @@ -95,7 +95,7 @@ int JVMSupport::asyncGetCallTrace(ASGCT_CallFrame *frames, int max_depth, void* return 0; } - AsyncSampleMutex mutex(ProfiledThread::currentSignalSafe()); + AsyncSampleMutex mutex(ProfiledThread::current()); if (!mutex.acquired()) { return 0; } diff --git a/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp b/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp index bbf65e77a9..1c08286d22 100644 --- a/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp +++ b/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp @@ -64,7 +64,7 @@ class RoutineInfo { // Unregister the current thread from the profiler and release its TLS under a // single SignalBlocker to close the race window between unregisterThread() // returning and release() acquiring its internal guard (PROF-14603). Without -// this, a SIGVTALRM delivered in that window could call currentSignalSafe() +// this, a SIGVTALRM delivered in that window could call current() // and dereference a now-freed ProfiledThread. Kept noinline so the // SignalBlocker's sigset_t does not appear in the caller's stack frame on // musl/aarch64 where the deopt blob may corrupt the wrapper's stack guard. diff --git a/ddprof-lib/src/main/cpp/perfEvents_linux.cpp b/ddprof-lib/src/main/cpp/perfEvents_linux.cpp index 40dc2e7107..6eae577f5f 100644 --- a/ddprof-lib/src/main/cpp/perfEvents_linux.cpp +++ b/ddprof-lib/src/main/cpp/perfEvents_linux.cpp @@ -741,7 +741,7 @@ void PerfEvents::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) { if (!cs.entered()) { return; // Another critical section is active, defer profiling } - ProfiledThread *current = ProfiledThread::currentSignalSafe(); + ProfiledThread *current = ProfiledThread::current(); if (current != NULL) { current->noteCPUSample(Profiler::instance()->recordingEpoch()); } diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 5c9da37762..240e3ab427 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -93,7 +93,7 @@ void Profiler::onThreadStart(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread) { } void Profiler::onThreadEnd(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread) { - ProfiledThread *current = ProfiledThread::currentSignalSafe(); + ProfiledThread *current = ProfiledThread::current(); int tid = -1; if (current != nullptr) { @@ -627,7 +627,7 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, call_trace_id = _call_trace_storage.put(num_frames, frames, truncated, counter); - ProfiledThread *thread = ProfiledThread::currentSignalSafe(); + ProfiledThread *thread = ProfiledThread::current(); if (thread != nullptr) { thread->recordCallTraceId(call_trace_id); } @@ -918,7 +918,7 @@ void Profiler::busHandler(int signo, siginfo_t *siginfo, void *ucontext) { // Returns: 0 = not handled (chain to next handler), non-zero = handled int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext) { - ProfiledThread* thrd = ProfiledThread::currentSignalSafe(); + ProfiledThread* thrd = ProfiledThread::current(); // First, try to handle safefetch - this doesn't need TLS or any protection // because it directly checks the PC and modifies ucontext to skip the fault. diff --git a/ddprof-lib/src/main/cpp/refCountGuard.cpp b/ddprof-lib/src/main/cpp/refCountGuard.cpp index a594aa3eeb..5f37d74a43 100644 --- a/ddprof-lib/src/main/cpp/refCountGuard.cpp +++ b/ddprof-lib/src/main/cpp/refCountGuard.cpp @@ -24,7 +24,7 @@ int RefCountGuard::slot_owners[RefCountGuard::MAX_THREADS]; static std::atomic s_outer_stack_overflow_warned{false}; int RefCountGuard::getThreadRefCountSlot() { - ProfiledThread* thrd = ProfiledThread::currentSignalSafe(); + ProfiledThread* thrd = ProfiledThread::current(); int tid = thrd != nullptr ? thrd->tid() : OS::threadId(); HashProbe probe(static_cast(tid), MAX_THREADS); diff --git a/ddprof-lib/src/main/cpp/signalSafety.h b/ddprof-lib/src/main/cpp/signalSafety.h index c7371d84c7..44d0d5d4b9 100644 --- a/ddprof-lib/src/main/cpp/signalSafety.h +++ b/ddprof-lib/src/main/cpp/signalSafety.h @@ -18,7 +18,7 @@ #define _SIGNAL_SAFETY_H #include "guards.h" // isInSignalContext, SIGNAL_HANDLER_GUARD, ... -#include "threadLocalData.h" // ProfiledThread::currentSignalSafe +#include "threadLocalData.h" // ProfiledThread::current // Detect ASAN using compiler-provided macros so the ASAN_ENABLED guard below // works in every TU that includes this header, independent of include order. @@ -63,7 +63,7 @@ #define DEBUG_ASSERT_NOT_IN_SIGNAL() \ do { \ - ProfiledThread *_pt_for_assert = ProfiledThread::currentSignalSafe(); \ + ProfiledThread *_pt_for_assert = ProfiledThread::current(); \ /* Skip when no thread context — see comment above. */ \ if (_pt_for_assert != nullptr && _pt_for_assert->signalDepth() != 0) { \ static const char _msg[] = \ diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index dfac3bc743..1c0ea19e58 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -140,10 +140,6 @@ class ProfiledThread : public ThreadLocalData { return _current_thread.get(); } - static inline ProfiledThread *currentSignalSafe() { // Signal-safe version that never allocates - return current(); - } - static int currentTid(); inline int tid() { return _tid; } inline u64 noteCPUSample(u32 recording_epoch) { diff --git a/ddprof-lib/src/main/cpp/wallClock.cpp b/ddprof-lib/src/main/cpp/wallClock.cpp index c29365dfcf..eac9f3fd37 100644 --- a/ddprof-lib/src/main/cpp/wallClock.cpp +++ b/ddprof-lib/src/main/cpp/wallClock.cpp @@ -236,7 +236,7 @@ void WallClockASGCT::signalHandler(int signo, siginfo_t *siginfo, void *ucontext if (!cs.entered()) { return; // Another critical section is active, defer profiling } - ProfiledThread *current = ProfiledThread::currentSignalSafe(); + ProfiledThread *current = ProfiledThread::current(); // Guard against the race window between Profiler::registerThread() and // thread_native_entry setting JVM TLS (PROF-13072): skip at most one signal // per thread. Pure native threads (where JVMThread::current() is always null) @@ -446,7 +446,7 @@ void WallClockJvmti::signalHandler(int signo, siginfo_t *siginfo, return; } int saved_errno = errno; - ProfiledThread *current = ProfiledThread::currentSignalSafe(); + ProfiledThread *current = ProfiledThread::current(); if (current != nullptr && JVMThread::current() == nullptr && current->inInitWindow()) { current->tickInitWindow(); diff --git a/ddprof-lib/src/test/cpp/ddprof_ut.cpp b/ddprof-lib/src/test/cpp/ddprof_ut.cpp index c3926d4a44..5c4d2040b1 100644 --- a/ddprof-lib/src/test/cpp/ddprof_ut.cpp +++ b/ddprof-lib/src/test/cpp/ddprof_ut.cpp @@ -361,7 +361,7 @@ static DdprofGlobalSetup ddprof_global_setup; // Deterministic regression for the CriticalSection::_thread_ptr capture fix. // - // Bug: the old destructor re-fetched currentSignalSafe() at destruction time. + // Bug: the old destructor re-fetched current() at destruction time. // If TLS was cleared between the ctor and dtor (e.g. release() called inside // the CS scope as it was in the old onThreadEnd), the re-fetch returned nullptr // and exitCriticalSection() was silently skipped, leaving _in_critical_section diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index b7813c0028..1f434aa4b4 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -52,7 +52,7 @@ class ProfiledThreadTypeTest : public ::testing::Test { protected: void SetUp() override { ProfiledThread::initCurrentThread(); - _pt = ProfiledThread::currentSignalSafe(); + _pt = ProfiledThread::current(); ASSERT_NE(nullptr, _pt); } @@ -123,7 +123,7 @@ class CrashHandlerNestingTest : public ::testing::Test { protected: void SetUp() override { ProfiledThread::initCurrentThread(); - _pt = ProfiledThread::currentSignalSafe(); + _pt = ProfiledThread::current(); ASSERT_NE(nullptr, _pt); } @@ -210,7 +210,7 @@ class JmpCtxChainingTest : public ::testing::Test { protected: void SetUp() override { ProfiledThread::initCurrentThread(); - _pt = ProfiledThread::currentSignalSafe(); + _pt = ProfiledThread::current(); ASSERT_NE(nullptr, _pt); } diff --git a/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp b/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp index 6f775c6438..992e9de1d5 100644 --- a/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp +++ b/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp @@ -6,7 +6,7 @@ * * Root cause: SIGVTALRM delivered between ProfiledThread::release() clearing TLS * (pthread_setspecific(NULL)) and deleting the ProfiledThread object, causing - * a signal handler to call currentSignalSafe() and dereference a freed pointer. + * a signal handler to call current() and dereference a freed pointer. * * Fix coverage: * - Signal-safe TLS accessor returns null in the race window (no crash). @@ -63,12 +63,12 @@ struct SigGuard { SigGuard& operator=(const SigGuard&) = delete; }; -// ── T-01: currentSignalSafe() is non-null while live, null after release ───── +// ── T-01: current() is non-null while live, null after release ───── static std::atomic g_t01_seen{nullptr}; static void t01_handler(int) { - g_t01_seen.store(ProfiledThread::currentSignalSafe(), std::memory_order_relaxed); + g_t01_seen.store(ProfiledThread::current(), std::memory_order_relaxed); } static void *t01_body(void *) { @@ -84,7 +84,7 @@ static void *t01_body(void *) { return nullptr; } EXPECT_NE(nullptr, t01_pre) - << "currentSignalSafe() must return non-null while ProfiledThread is live"; + << "current() must return non-null while ProfiledThread is live"; ProfiledThread::release(); @@ -96,7 +96,7 @@ static void *t01_body(void *) { return nullptr; } EXPECT_EQ(nullptr, t01_post) - << "currentSignalSafe() must return null after release()"; + << "current() must return null after release()"; return nullptr; } @@ -113,7 +113,7 @@ TEST(ThreadTeardownSafetyTest, TLSAccessibleDuringLifetimeNullAfterRelease) { static std::atomic g_t02_seen{nullptr}; static void t02_handler(int) { - g_t02_seen.store(ProfiledThread::currentSignalSafe(), std::memory_order_relaxed); + g_t02_seen.store(ProfiledThread::current(), std::memory_order_relaxed); } static void *t02_body(void *) { @@ -129,7 +129,7 @@ static void *t02_body(void *) { // Signal delivered in the race window must see null, not a dangling pointer. pthread_kill(pthread_self(), SIGVTALRM); EXPECT_EQ(nullptr, g_t02_seen.load(std::memory_order_relaxed)) - << "currentSignalSafe() must return null in the TLS-clear/delete window"; + << "current() must return null in the TLS-clear/delete window"; // release() with TLS already null must not double-free. ProfiledThread::release(); @@ -164,7 +164,7 @@ TEST(ThreadTeardownSafetyTest, DoubleReleaseIsIdempotent) { static std::atomic g_t04_seen{nullptr}; static void t04_handler(int) { - g_t04_seen.store(ProfiledThread::currentSignalSafe(), std::memory_order_relaxed); + g_t04_seen.store(ProfiledThread::current(), std::memory_order_relaxed); } static void *t04_body(void *) { @@ -174,7 +174,7 @@ static void *t04_body(void *) { g_t04_seen.store(kNotYetRun, std::memory_order_relaxed); pthread_kill(pthread_self(), SIGVTALRM); EXPECT_EQ(nullptr, g_t04_seen.load(std::memory_order_relaxed)) - << "currentSignalSafe() must return null for unregistered threads"; + << "current() must return null for unregistered threads"; return nullptr; } @@ -189,7 +189,7 @@ TEST(ThreadTeardownSafetyTest, SignalSafeAccessorReturnsNullWithoutInit) { static std::atomic g_t05_signal_count{0}; static void t05_handler(int) { - (void)ProfiledThread::currentSignalSafe(); + (void)ProfiledThread::current(); g_t05_signal_count.fetch_add(1, std::memory_order_relaxed); } @@ -363,11 +363,11 @@ TEST(ThreadTeardownSafetyTest, ForcedUnwindWithConcurrentSignalDoesNotCrash) { static void *t08_body(void *) { ProfiledThread::initCurrentThread(); - ProfiledThread *first = ProfiledThread::currentSignalSafe(); + ProfiledThread *first = ProfiledThread::current(); EXPECT_NE(nullptr, first); ProfiledThread::initCurrentThread(); // second call on the same thread - ProfiledThread *second = ProfiledThread::currentSignalSafe(); + ProfiledThread *second = ProfiledThread::current(); EXPECT_NE(nullptr, second); EXPECT_EQ(first, second) << "double init must not allocate a second ProfiledThread"; @@ -387,7 +387,7 @@ static std::atomic g_t09_stop{false}; static std::atomic g_t09_signal_count{0}; static void t09_handler(int) { - (void)ProfiledThread::currentSignalSafe(); + (void)ProfiledThread::current(); g_t09_signal_count.fetch_add(1, std::memory_order_relaxed); } @@ -434,9 +434,9 @@ TEST(ThreadTeardownSafetyTest, HighFrequencySignalsDuringThreadChurn) { static void *t10_body(void *) { ProfiledThread::initCurrentThread(); - ProfiledThread *pt = ProfiledThread::currentSignalSafe(); + ProfiledThread *pt = ProfiledThread::current(); if (pt == nullptr) { - ADD_FAILURE() << "currentSignalSafe() returned nullptr"; + ADD_FAILURE() << "current() returned nullptr"; return nullptr; } From f766961e6655609563dc44abe0391488f0b10134 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 8 Jul 2026 20:43:13 +0000 Subject: [PATCH 09/19] Pre-existed - add nullptr check --- ddprof-lib/src/main/cpp/profiler.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 240e3ab427..2e84e930a3 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -74,8 +74,11 @@ static CTimer ctimer; static CTimerJvmti ctimer_jvmti; void Profiler::onThreadStart(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread) { - ProfiledThread::initCurrentThread(); - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread* current = ProfiledThread::initCurrentThread(); + if (current == nullptr) { + return; + } + current->setJavaThread(true); int tid = current->tid(); if (_thread_filter.enabled()) { From 63d7b6190e27ccd2466a124c1237de51b14bb725 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 8 Jul 2026 21:05:10 +0000 Subject: [PATCH 10/19] Fix --- ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp b/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp index 992e9de1d5..08e871ed69 100644 --- a/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp +++ b/ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp @@ -133,6 +133,9 @@ static void *t02_body(void *) { // release() with TLS already null must not double-free. ProfiledThread::release(); + // Complete the simulated teardown: delete the object (mirrors what freeKey + // would do). Destructor is private so we need the test helper. + ProfiledThread::deleteForTest(detached); EXPECT_EQ(nullptr, ProfiledThread::current()); return nullptr; } From e5d59e7dc7b37d7faa5c0880af3d9e0eee81200a Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 9 Jul 2026 12:31:08 +0000 Subject: [PATCH 11/19] Restore early behavior --- ddprof-lib/src/main/cpp/javaApi.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index 89eb9f91f6..ba7eed8035 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -139,7 +139,7 @@ Java_com_datadoghq_profiler_JavaProfiler_getSamples(JNIEnv *env, // Init or get current profiled thread. // Calling thread's thread local may not be initialized due to race. // Especially, during the early startup phase. -// This call is expensive, should be avoided on hot paths. +// This call could be expensive, if TLS has not yet set. // Note: the racy can be avoided with native agent, remove this method // once converted to native agent. static ProfiledThread* initOrGetCurrentThread() { @@ -158,7 +158,7 @@ static ProfiledThread* initOrGetCurrentThread() { // still compatible in the event of signature changes in the future. extern "C" DLLEXPORT void JNICALL JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() { - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = initOrGetCurrentThread(); if(current == nullptr) { return; } From 9ded4c2554e2fb7cabed53d0318398b7280731f9 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 9 Jul 2026 13:00:02 +0000 Subject: [PATCH 12/19] Harden initCurrentThread() with initCurrentThreadSignalSafe() --- ddprof-lib/src/main/cpp/profiler.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 2e84e930a3..ad58856af2 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -74,7 +74,8 @@ static CTimer ctimer; static CTimerJvmti ctimer_jvmti; void Profiler::onThreadStart(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread) { - ProfiledThread* current = ProfiledThread::initCurrentThread(); + // JVMTI callback - outside signal handeler + ProfiledThread* current = ProfiledThread::initCurrentThreadSignalSafe(); if (current == nullptr) { return; } @@ -1245,7 +1246,8 @@ Error Profiler::init() { return error; } - ProfiledThread::initCurrentThread(); + // JNI down call, outside signal handler + ProfiledThread::initCurrentThreadSignalSafe(); return Error::OK; } @@ -1370,7 +1372,7 @@ Error Profiler::start(Arguments &args, bool reset) { // Minor optim: Register the current thread (start thread won't be called) if (_thread_filter.enabled()) { _thread_filter.clearActive(); - ProfiledThread *current = ProfiledThread::initCurrentThread(); + ProfiledThread *current = ProfiledThread::initCurrentThreadSignalSafe(); assert(current != nullptr); int slot_id = current->filterSlotId(); if (slot_id < 0) { From 2d1088337deaf843d18ea71585fe9be2a3e0cb37 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 9 Jul 2026 14:55:41 +0000 Subject: [PATCH 13/19] Block signal for deleting ProfiledThread --- ddprof-lib/src/main/cpp/threadLocalData.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ddprof-lib/src/main/cpp/threadLocalData.cpp b/ddprof-lib/src/main/cpp/threadLocalData.cpp index 06ddb9109e..76f4231b07 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.cpp +++ b/ddprof-lib/src/main/cpp/threadLocalData.cpp @@ -33,6 +33,7 @@ ProfiledThread* ProfiledThread::initCurrentThreadSignalSafe() { } void ProfiledThread::freeValue(void* value) { + SignalBlocker blocker; ProfiledThread* pt = reinterpret_cast(value); delete pt; } From d59a7c2f1f2bcafeefabfaa28c47c905117e6e93 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 9 Jul 2026 18:57:17 +0000 Subject: [PATCH 14/19] Review comments --- ddprof-lib/src/main/cpp/javaApi.cpp | 19 +++++++++++++++++-- ddprof-lib/src/main/cpp/threadLocalData.h | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index ba7eed8035..633dceca29 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -142,6 +142,8 @@ Java_com_datadoghq_profiler_JavaProfiler_getSamples(JNIEnv *env, // This call could be expensive, if TLS has not yet set. // Note: the racy can be avoided with native agent, remove this method // once converted to native agent. +// TODO: Priming is not fully restored at this moment, this method only +// handles a few special cases. static ProfiledThread* initOrGetCurrentThread() { ProfiledThread* current = ProfiledThread::current(); if (current == nullptr) { @@ -158,6 +160,7 @@ static ProfiledThread* initOrGetCurrentThread() { // still compatible in the event of signature changes in the future. extern "C" DLLEXPORT void JNICALL JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() { + // Initialize thread TLS if it has not yet done ProfiledThread *current = initOrGetCurrentThread(); if(current == nullptr) { return; @@ -191,7 +194,8 @@ JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() { extern "C" DLLEXPORT void JNICALL JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadRemove0() { - ProfiledThread *current = ProfiledThread::current(); + // Initialize thread TLS if it has not yet done + ProfiledThread *current = initOrGetCurrentThread(); if(current == nullptr) { return; } @@ -231,6 +235,10 @@ Java_com_datadoghq_profiler_JavaProfiler_recordTrace0( JNIEnv *env, jclass unused, jlong rootSpanId, jstring endpoint, jstring operation, jint sizeLimit) { JniString endpoint_str(env, endpoint); + + // Initialize thread TLS if it has not yet done + initOrGetCurrentThread(); + u32 endpointLabel = Profiler::instance()->stringLabelMap()->bounded_lookup( endpoint_str.c_str(), endpoint_str.length(), sizeLimit); // StringDictionary reserves 0 as "no entry"; valid IDs start at 1. @@ -288,6 +296,9 @@ Java_com_datadoghq_profiler_JavaProfiler_describeDebugCounters0( extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_recordSettingEvent0( JNIEnv *env, jclass unused, jstring name, jstring value, jstring unit) { + // Initialize thread TLS if it has not yet done + initOrGetCurrentThread(); + int tid = ProfiledThread::currentTid(); if (tid < 0) { return; @@ -312,6 +323,10 @@ extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_recordQueueEnd0( JNIEnv *env, jclass unused, jlong startTime, jlong endTime, jstring task, jstring scheduler, jthread origin, jstring queueType, jint queueLength) { + + // Initialize thread TLS if it has not yet done + initOrGetCurrentThread(); + int tid = ProfiledThread::currentTid(); if (tid < 0) { return; @@ -713,7 +728,7 @@ Java_com_datadoghq_profiler_OTelContext_readProcessCtx0(JNIEnv *env, jclass unus extern "C" DLLEXPORT jobject JNICALL Java_com_datadoghq_profiler_JavaProfiler_initializeContextTLS0(JNIEnv* env, jclass unused, jlongArray metadata) { - // Assume this call is not on hot path + // Initialize thread TLS if it has not yet done ProfiledThread* thrd = initOrGetCurrentThread(); assert(thrd != nullptr); diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 1c0ea19e58..c93dfda2e7 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -124,7 +124,7 @@ class ProfiledThread : public ThreadLocalData { static void deleteForTest(ProfiledThread *pt) { delete pt; } #endif // initCurrentThread() and release() are not async-signal-safe: - // must call outside of a signal handler with signal blocked + // must be called outside of a signal handler with signal blocked static ProfiledThread* initCurrentThread(); static void release(); From 00edbd4548daf1c79ea48ac9ecfed4793104ce9d Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 10 Jul 2026 14:43:30 +0000 Subject: [PATCH 15/19] Review comments --- ddprof-lib/src/main/cpp/javaApi.cpp | 8 ++++---- ddprof-lib/src/main/cpp/profiler.cpp | 19 ++++++++++++++++--- ddprof-lib/src/main/cpp/threadLocal.h | 18 ++---------------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index 633dceca29..cac9cf9879 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -361,7 +361,7 @@ Java_com_datadoghq_profiler_JavaProfiler_recordQueueEnd0( extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(JNIEnv *env, jclass unused) { - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = initOrGetCurrentThread(); if (current == nullptr) { return; } @@ -379,7 +379,7 @@ Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(JNIEnv *env, jclass unused) extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_parkExit0( JNIEnv *env, jclass unused, jlong blocker, jlong unblockingSpanId) { - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = initOrGetCurrentThread(); if (current == nullptr) { return; } @@ -413,7 +413,7 @@ Java_com_datadoghq_profiler_JavaProfiler_blockEnter0( if (!decodeJavaBlockState(state, decoded)) { return 0; } - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = initOrGetCurrentThread(); if (current == nullptr) { return 0; } @@ -435,7 +435,7 @@ Java_com_datadoghq_profiler_JavaProfiler_blockExit0( if (block_token == 0) { return; } - ProfiledThread *current = ProfiledThread::current(); + ProfiledThread *current = initOrGetCurrentThread(); if (current == nullptr) { return; } diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index ad58856af2..5e615138bf 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1241,10 +1241,23 @@ Error Profiler::checkState() { Error Profiler::init() { MutexLocker ml(_state_lock); - Error error = checkState(); - if (error) { - return error; + State s = state(); + if (s == ERROR) { + return Error("Profiler encountered fatal error"); + } else if (s == NEW) { + // Make sure JVMSupport is initialized + // In theory, it should be initialized in JVMTI::VMInit() callback, + // but the callback arrives too late, after this method is called. + if (!JVMSupport::initialize()) { + _state.store(ERROR, std::memory_order_release); + return Error("Profiler encountered fatal error"); + } } + // Unlike checkState(), init() does not reject a RUNNING/TERMINATED + // profiler: it is idempotent per-thread setup for the Java API + // (e.g. JavaProfiler.getInstance()), not a state transition, and the + // agent may already be RUNNING by the time it is called (VMInit can + // auto-start the profiler before Java code gets a chance to call in). // JNI down call, outside signal handler ProfiledThread::initCurrentThreadSignalSafe(); diff --git a/ddprof-lib/src/main/cpp/threadLocal.h b/ddprof-lib/src/main/cpp/threadLocal.h index 888e7a7249..ffd06713a0 100644 --- a/ddprof-lib/src/main/cpp/threadLocal.h +++ b/ddprof-lib/src/main/cpp/threadLocal.h @@ -58,6 +58,8 @@ typedef void (*CLEAN_FUNC)(void*); static constexpr pthread_key_t INVALID_KEY = pthread_key_t(-1); +// We don't delete keys (pthread_key_delete()) due to various race scenarios, but +// it is okay, because profiler is considered immortal. template class ThreadLocal { protected: @@ -80,14 +82,6 @@ class ThreadLocal { assert(isKeyValid()); } - ~ThreadLocal() { - if(isKeyValid()) { - pthread_key_delete(_key); - } else { - assert(false && "Invalid pthread key"); - } - } - bool isKeyValid() const { return _key != INVALID_KEY; } @@ -147,14 +141,6 @@ class ThreadLocal { assert(isKeyValid() && "Invalid pthread key"); } - ~ThreadLocal() { - if(isKeyValid()) { - pthread_key_delete(_key); - } else { - assert(isKeyValid() && "Invalid pthread key"); - } - } - bool isKeyValid() const { return _key != INVALID_KEY; } From 800d38db457f95373b348ebf1c12e19869e68004 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 10 Jul 2026 16:28:18 +0000 Subject: [PATCH 16/19] Review --- .../src/main/cpp/hotspot/jitCodeCache.cpp | 3 ++ ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp | 2 ++ ddprof-lib/src/main/cpp/javaApi.cpp | 36 ++++++------------- ddprof-lib/src/main/cpp/livenessTracker.cpp | 1 + ddprof-lib/src/main/cpp/objectSampler.cpp | 1 + ddprof-lib/src/main/cpp/profiler.cpp | 1 + ddprof-lib/src/main/cpp/threadLocalData.cpp | 13 +++++-- ddprof-lib/src/main/cpp/vmEntry.cpp | 9 +++++ ddprof-lib/src/main/cpp/vmEntry.h | 4 +-- 9 files changed, 39 insertions(+), 31 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/jitCodeCache.cpp b/ddprof-lib/src/main/cpp/hotspot/jitCodeCache.cpp index 85af3d19f8..00ec522bfe 100644 --- a/ddprof-lib/src/main/cpp/hotspot/jitCodeCache.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/jitCodeCache.cpp @@ -6,6 +6,7 @@ #include "jitCodeCache.h" #include "hotspot/vmStructs.h" +#include "threadLocalData.h" SpinLock JitCodeCache::_stubs_lock; CodeCache JitCodeCache::_runtime_stubs("[stubs]"); @@ -19,11 +20,13 @@ void JNICALL JitCodeCache::CompiledMethodLoad(jvmtiEnv *jvmti, jmethodID method, jint map_length, const jvmtiAddrLocationMap *map, const void *compile_info) { + ProfiledThread::initCurrentThreadSignalSafe(); CodeHeap::updateBounds(code_addr, (const char *)code_addr + code_size); } void JNICALL JitCodeCache::DynamicCodeGenerated(jvmtiEnv *jvmti, const char *name, const void *address, jint length) { + ProfiledThread::initCurrentThreadSignalSafe(); _stubs_lock.lock(); _runtime_stubs.add(address, length, name, true); _stubs_lock.unlock(); diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp index d5453ce917..ccbca45089 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp @@ -597,6 +597,8 @@ void JNICALL VMStructs::NativeMethodBind(jvmtiEnv *jvmti, JNIEnv *jni, jthread t static int delayedCounter = 0; static void **delayed = (void **)malloc(512 * sizeof(void *) * 2); + ProfiledThread::initCurrentThreadSignalSafe(); + if (_memory_usage_func == NULL) { if (jvmti != NULL && jni != NULL) { checkNativeBinding(jvmti, jni, method, address); diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index cac9cf9879..cf23c5c351 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -136,22 +136,6 @@ Java_com_datadoghq_profiler_JavaProfiler_getSamples(JNIEnv *env, return (jlong)Profiler::instance()->total_samples(); } -// Init or get current profiled thread. -// Calling thread's thread local may not be initialized due to race. -// Especially, during the early startup phase. -// This call could be expensive, if TLS has not yet set. -// Note: the racy can be avoided with native agent, remove this method -// once converted to native agent. -// TODO: Priming is not fully restored at this moment, this method only -// handles a few special cases. -static ProfiledThread* initOrGetCurrentThread() { - ProfiledThread* current = ProfiledThread::current(); - if (current == nullptr) { - current = ProfiledThread::initCurrentThreadSignalSafe(); - } - return current; -} - // some duplication between add and remove, though we want to avoid having an extra branch in the hot path // JavaCritical is faster JNI, but more restrictive - parameters and return value have to be @@ -161,7 +145,7 @@ static ProfiledThread* initOrGetCurrentThread() { extern "C" DLLEXPORT void JNICALL JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() { // Initialize thread TLS if it has not yet done - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::initCurrentThreadSignalSafe(); if(current == nullptr) { return; } @@ -195,7 +179,7 @@ JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() { extern "C" DLLEXPORT void JNICALL JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadRemove0() { // Initialize thread TLS if it has not yet done - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::initCurrentThreadSignalSafe(); if(current == nullptr) { return; } @@ -237,7 +221,7 @@ Java_com_datadoghq_profiler_JavaProfiler_recordTrace0( JniString endpoint_str(env, endpoint); // Initialize thread TLS if it has not yet done - initOrGetCurrentThread(); + ProfiledThread::initCurrentThreadSignalSafe(); u32 endpointLabel = Profiler::instance()->stringLabelMap()->bounded_lookup( endpoint_str.c_str(), endpoint_str.length(), sizeLimit); @@ -297,7 +281,7 @@ extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_recordSettingEvent0( JNIEnv *env, jclass unused, jstring name, jstring value, jstring unit) { // Initialize thread TLS if it has not yet done - initOrGetCurrentThread(); + ProfiledThread::initCurrentThreadSignalSafe(); int tid = ProfiledThread::currentTid(); if (tid < 0) { @@ -325,7 +309,7 @@ Java_com_datadoghq_profiler_JavaProfiler_recordQueueEnd0( jstring scheduler, jthread origin, jstring queueType, jint queueLength) { // Initialize thread TLS if it has not yet done - initOrGetCurrentThread(); + ProfiledThread::initCurrentThreadSignalSafe(); int tid = ProfiledThread::currentTid(); if (tid < 0) { @@ -361,7 +345,7 @@ Java_com_datadoghq_profiler_JavaProfiler_recordQueueEnd0( extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(JNIEnv *env, jclass unused) { - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::initCurrentThreadSignalSafe(); if (current == nullptr) { return; } @@ -379,7 +363,7 @@ Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(JNIEnv *env, jclass unused) extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_parkExit0( JNIEnv *env, jclass unused, jlong blocker, jlong unblockingSpanId) { - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::initCurrentThreadSignalSafe(); if (current == nullptr) { return; } @@ -413,7 +397,7 @@ Java_com_datadoghq_profiler_JavaProfiler_blockEnter0( if (!decodeJavaBlockState(state, decoded)) { return 0; } - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::initCurrentThreadSignalSafe(); if (current == nullptr) { return 0; } @@ -435,7 +419,7 @@ Java_com_datadoghq_profiler_JavaProfiler_blockExit0( if (block_token == 0) { return; } - ProfiledThread *current = initOrGetCurrentThread(); + ProfiledThread *current = ProfiledThread::initCurrentThreadSignalSafe(); if (current == nullptr) { return; } @@ -729,7 +713,7 @@ Java_com_datadoghq_profiler_OTelContext_readProcessCtx0(JNIEnv *env, jclass unus extern "C" DLLEXPORT jobject JNICALL Java_com_datadoghq_profiler_JavaProfiler_initializeContextTLS0(JNIEnv* env, jclass unused, jlongArray metadata) { // Initialize thread TLS if it has not yet done - ProfiledThread* thrd = initOrGetCurrentThread(); + ProfiledThread* thrd = ProfiledThread::initCurrentThreadSignalSafe(); assert(thrd != nullptr); if (!thrd->isContextInitialized()) { diff --git a/ddprof-lib/src/main/cpp/livenessTracker.cpp b/ddprof-lib/src/main/cpp/livenessTracker.cpp index ac471463cd..585d271da7 100644 --- a/ddprof-lib/src/main/cpp/livenessTracker.cpp +++ b/ddprof-lib/src/main/cpp/livenessTracker.cpp @@ -409,6 +409,7 @@ void LivenessTracker::track(JNIEnv *env, AllocEvent &event, jint tid, } void JNICALL LivenessTracker::GarbageCollectionFinish(jvmtiEnv *jvmti_env) { + ProfiledThread::initCurrentThreadSignalSafe(); LivenessTracker::instance()->onGC(); } diff --git a/ddprof-lib/src/main/cpp/objectSampler.cpp b/ddprof-lib/src/main/cpp/objectSampler.cpp index b6bf90e47a..1cc4cabe38 100644 --- a/ddprof-lib/src/main/cpp/objectSampler.cpp +++ b/ddprof-lib/src/main/cpp/objectSampler.cpp @@ -50,6 +50,7 @@ bool ObjectSampler::normalizeClassSignature(const char *class_name, void ObjectSampler::SampledObjectAlloc(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread, jobject object, jclass object_klass, jlong size) { + ProfiledThread::initCurrentThreadSignalSafe(); ObjectSampler::instance()->recordAllocation(jvmti, jni, thread, BCI_ALLOC, object, object_klass, size); } diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 5e615138bf..ffc2045dde 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -97,6 +97,7 @@ void Profiler::onThreadStart(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread) { } void Profiler::onThreadEnd(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread) { + // Thread teardown: no point to intialize TLS if not yet initialized ProfiledThread *current = ProfiledThread::current(); int tid = -1; diff --git a/ddprof-lib/src/main/cpp/threadLocalData.cpp b/ddprof-lib/src/main/cpp/threadLocalData.cpp index 76f4231b07..21224bc3ee 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.cpp +++ b/ddprof-lib/src/main/cpp/threadLocalData.cpp @@ -28,8 +28,17 @@ ProfiledThread* ProfiledThread::initCurrentThread() { } ProfiledThread* ProfiledThread::initCurrentThreadSignalSafe() { - SignalBlocker blocker; - return initCurrentThread(); + if (!isThreadKeyValid()) { + return nullptr; + } + + ProfiledThread* cur = current(); + if (cur == nullptr) { + SignalBlocker blocker; + return initCurrentThread(); + } else { + return cur; + } } void ProfiledThread::freeValue(void* value) { diff --git a/ddprof-lib/src/main/cpp/vmEntry.cpp b/ddprof-lib/src/main/cpp/vmEntry.cpp index bcea72d07f..8eadc643d1 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.cpp +++ b/ddprof-lib/src/main/cpp/vmEntry.cpp @@ -18,6 +18,7 @@ #include "os.h" #include "profiler.h" #include "safeAccess.h" +#include "threadLocalData.h" // Pulls in vmStructs.h plus the definitions of crashProtectionActive()/cast_to() that its inline // accessors odr-use here; the light vmStructs.h alone leaves those unresolved in assertion-enabled // builds (see the note in hotspotStackFrame_aarch64.cpp). @@ -594,9 +595,17 @@ void *VM::getLibraryHandle(const char *name) { void JNICALL VM::ClassPrepare(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jclass klass) { + ProfiledThread::initCurrentThreadSignalSafe(); JVMSupport::loadMethodIDsIfNeeded(jvmti, jni, klass); } +void JNICALL VM::ClassLoad(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread, + jclass klass) { + // Needed only for AsyncGetCallTrace support + ProfiledThread::initCurrentThreadSignalSafe(); +} + + void JNICALL VM::VMInit(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread) { ready(jvmti, jni); diff --git a/ddprof-lib/src/main/cpp/vmEntry.h b/ddprof-lib/src/main/cpp/vmEntry.h index d97fa6af3a..875e7c39b1 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.h +++ b/ddprof-lib/src/main/cpp/vmEntry.h @@ -237,9 +237,7 @@ class VM { static void JNICALL VMDeath(jvmtiEnv *jvmti, JNIEnv *jni); static void JNICALL ClassLoad(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread, - jclass klass) { - // Needed only for AsyncGetCallTrace support - } + jclass klass); static void JNICALL ClassPrepare(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jclass klass); From 34b18f0f66fa0559c9fe709660547d0e6d4adcfa Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 10 Jul 2026 18:30:17 +0000 Subject: [PATCH 17/19] Profiler must be initalized on non-virtual thread --- .../com/datadoghq/profiler/JavaProfiler.java | 30 +++++++++++++++++ .../datadoghq/profiler/ExternalLauncher.java | 32 +++++++++++++++++++ .../datadoghq/profiler/JavaProfilerTest.java | 25 +++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java b/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java index f925f8fd4a..9f99ae85f1 100644 --- a/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java +++ b/ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java @@ -18,6 +18,7 @@ package com.datadoghq.profiler; import java.io.IOException; +import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.file.Path; @@ -39,6 +40,30 @@ static final class TSCFrequencyHolder { } private static JavaProfiler instance; + // Thread.isVirtual() was added in JDK 21; resolved reflectively (once) so this class + // still compiles against a JDK 17 (or older) bootclasspath. null means the JVM this + // code is running on predates virtual threads, so no thread can ever be one. + private static final Method IS_VIRTUAL_METHOD = resolveIsVirtualMethod(); + + private static Method resolveIsVirtualMethod() { + try { + return Thread.class.getMethod("isVirtual"); + } catch (NoSuchMethodException e) { + return null; + } + } + + private static boolean isVirtualThread(Thread thread) { + if (IS_VIRTUAL_METHOD == null) { + return false; + } + try { + return (Boolean) IS_VIRTUAL_METHOD.invoke(thread); + } catch (ReflectiveOperationException e) { + return false; + } + } + // Storage for profiling context. Scoped to the carrier thread when available so a // mounted virtual thread resolves to its current carrier's OTEP record (the record the // sampler reads); falls back to plain thread-local storage otherwise. See @@ -104,6 +129,11 @@ public static synchronized JavaProfiler getInstance(String libLocation, String s if (!result.succeeded) { throw new IOException("Failed to load Datadog Java profiler library", result.error); } + + if (isVirtualThread(Thread.currentThread())) { + throw new IOException("Cannot intialize profiler on an virtual thread"); + } + init0(); instance = profiler; diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java b/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java index cee538aeaa..695412dcb0 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java @@ -1,7 +1,14 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler; +import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; +import java.lang.reflect.Method; import java.util.Random; import java.util.concurrent.atomic.LongAdder; @@ -15,9 +22,22 @@ *
  • library - loads the profiler library
  • *
  • profiler [comma delimited profiler command list] - starts the profiler
  • *
  • profiler-work: [comma delimited profiler command list] - starts the profiler and runs a CPU-intensive task
  • + *
  • profiler-virtual-thread - calls {@link JavaProfiler#getInstance()} for the first time from a virtual thread
  • * */ public class ExternalLauncher { + /** + * Starts a virtual thread using reflection so this class compiles with {@code --release 8}. + * Equivalent to {@code Thread.ofVirtual().start(task)}. + */ + private static Thread startVirtualThread(Runnable task) throws Exception { + Method ofVirtual = Thread.class.getMethod("ofVirtual"); + Object builder = ofVirtual.invoke(null); + Class builderInterface = Class.forName("java.lang.Thread$Builder"); + Method start = builderInterface.getMethod("start", Runnable.class); + return (Thread) start.invoke(builder, task); + } + public static void main(String[] args) throws Exception { Thread worker = null; try { @@ -26,6 +46,18 @@ public static void main(String[] args) throws Exception { } if (args[0].equals("library")) { JVMAccess.getInstance(); + } else if (args[0].equals("profiler-virtual-thread")) { + Thread vt = startVirtualThread(() -> { + try { + JavaProfiler.getInstance(); + System.out.println("[virtual-thread-no-exception]"); + } catch (IOException e) { + System.out.println("[virtual-thread-ioexception] " + e.getMessage()); + } catch (Throwable t) { + System.out.println("[virtual-thread-unexpected] " + t.getClass().getName() + ": " + t.getMessage()); + } + }); + vt.join(); } else if (args[0].equals("profiler")) { JavaProfiler instance = JavaProfiler.getInstance(); if (args.length == 2) { diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JavaProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JavaProfilerTest.java index 5648b29f2a..2023c4757c 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/JavaProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JavaProfilerTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler; import org.junit.jupiter.api.Test; @@ -109,6 +114,26 @@ void testJ9ForceJvmtiSanity() throws Exception { assertFalse(hasWall.get()); } + @Test + void getInstanceFromVirtualThreadThrowsIOException() throws Exception { + assumeTrue(Platform.isJavaVersionAtLeast(21)); + + AtomicReference resultLine = new AtomicReference<>(); + boolean val = launch("profiler-virtual-thread", Collections.emptyList(), "", l -> { + if (l.startsWith("[virtual-thread-")) { + resultLine.set(l); + return LineConsumerResult.STOP; + } + return LineConsumerResult.CONTINUE; + }, null).inTime; + + assertTrue(val); + String result = resultLine.get(); + assertNotNull(result, "getInstance() did not report a result from the virtual thread"); + assertTrue(result.startsWith("[virtual-thread-ioexception]"), + "Expected IOException from getInstance() on a virtual thread, got: " + result); + } + @Test void vmStackwalkerCrashRecoveryTest() throws Exception { assumeFalse(Platform.isJ9() || Platform.isZing()); // J9 and Zing do not support vmstructs From dcaba6aaef8a2fa3cb78c5913d828fa11c63e608 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 10 Jul 2026 19:24:44 +0000 Subject: [PATCH 18/19] New methods after merge --- ddprof-lib/src/main/cpp/javaApi.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ddprof-lib/src/main/cpp/javaApi.cpp b/ddprof-lib/src/main/cpp/javaApi.cpp index 0b872f1cd3..83f6045c1a 100644 --- a/ddprof-lib/src/main/cpp/javaApi.cpp +++ b/ddprof-lib/src/main/cpp/javaApi.cpp @@ -878,7 +878,7 @@ extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_setTraceContext0(JNIEnv* env, jclass unused, jlong localRootSpanId, jlong spanId, jlong traceIdHigh, jlong traceIdLow, jint slot0, jint enc0, jbyteArray utf0, jint slot1, jint enc1, jbyteArray utf1) { - ProfiledThread* thrd = ProfiledThread::current(); + ProfiledThread* thrd = ProfiledThread::initCurrentThreadSignalSafe(); if (thrd == nullptr) { return; } @@ -934,7 +934,7 @@ Java_com_datadoghq_profiler_JavaProfiler_setTraceContext0(JNIEnv* env, jclass un // detached (valid=0), mirroring the DBB clear path (setContext(0,0,0,0) + clearContextValue*). extern "C" DLLEXPORT void JNICALL Java_com_datadoghq_profiler_JavaProfiler_clearTraceContext0(JNIEnv* env, jclass unused) { - ProfiledThread* thrd = ProfiledThread::current(); + ProfiledThread* thrd = ProfiledThread::initCurrentThreadSignalSafe(); if (thrd == nullptr) { return; } @@ -959,7 +959,7 @@ Java_com_datadoghq_profiler_JavaProfiler_clearTraceContext0(JNIEnv* env, jclass extern "C" DLLEXPORT jboolean JNICALL Java_com_datadoghq_profiler_JavaProfiler_setContextValue0(JNIEnv* env, jclass unused, jint slot, jint encoding, jbyteArray utf8) { - ProfiledThread* thrd = ProfiledThread::current(); + ProfiledThread* thrd = ProfiledThread::initCurrentThreadSignalSafe(); if (thrd == nullptr || slot < 0 || slot >= (jint)DD_TAGS_CAPACITY) { return JNI_FALSE; } From 42fd8988b8dd0585ea8cffdee01563f79f9d4dd4 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 10 Jul 2026 19:35:44 +0000 Subject: [PATCH 19/19] Comment JVMSupport::initialize() --- ddprof-lib/src/main/cpp/jvmSupport.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ddprof-lib/src/main/cpp/jvmSupport.cpp b/ddprof-lib/src/main/cpp/jvmSupport.cpp index 45c10332d4..783c34e458 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.cpp +++ b/ddprof-lib/src/main/cpp/jvmSupport.cpp @@ -20,6 +20,12 @@ volatile JVMSupport::JMethodIDLoadStats JVMSupport::jmethodID_load_state = JVMSupport::No_loaded; Mutex JVMSupport::_initialization_lock; + +// This method must be called after JVM has been properly initialized, e.g. after JVMTI::VMinit() +// callback. +// Currently, there are two paths lead to this call +// - JVMTI::VMInit() callback (vmEntry.cpp) +// - JavaProfiler.getInstance() via JNI down call - JVM must have been initialized bool JVMSupport::initialize() { MutexLocker locker(_initialization_lock);