Skip to content

Commit becce5a

Browse files
committed
fix(taskblock): harden JVM producer initialization
1 parent 06d8afe commit becce5a

8 files changed

Lines changed: 372 additions & 104 deletions

File tree

build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
116

217
package com.datadoghq.native.config
318

@@ -149,7 +164,7 @@ object ConfigurationPresets {
149164
config.compilerArgs.set(
150165
listOf("-O0", "-g", "-DDEBUG") + commonLinuxCompilerArgs(version)
151166
)
152-
config.linkerArgs.set(commonLinuxLinkerArgs())
167+
config.linkerArgs.set(commonLinuxLinkerArgs() + listOf("-Wl,-z,nodelete"))
153168
}
154169
Platform.MACOS -> {
155170
config.compilerArgs.set(

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

Lines changed: 22 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,20 @@ Java_com_datadoghq_profiler_JavaProfiler_init0(
7878
}
7979

8080
// JavaVM* has already been stored when the native library was loaded so we can pass nullptr here
81-
return VM::initProfilerBridge(nullptr, true, delegateMonitorWaitEvents);
81+
ProfilerBridgeInitResult result =
82+
VM::initProfilerBridge(nullptr, true, delegateMonitorWaitEvents);
83+
if (result == ProfilerBridgeInitResult::MONITOR_EVENTS_DELEGATION_CONFLICT) {
84+
throwNew(env, "java/lang/IllegalStateException",
85+
"Monitor-event ownership conflicts with the profiler's "
86+
"process-wide initialization");
87+
return JNI_FALSE;
88+
}
89+
if (result != ProfilerBridgeInitResult::SUCCESS) {
90+
throwNew(env, "java/lang/IllegalStateException",
91+
"Failed to initialize the profiler bridge");
92+
return JNI_FALSE;
93+
}
94+
return JNI_TRUE;
8295
}
8396

8497
extern "C" DLLEXPORT void JNICALL
@@ -144,32 +157,6 @@ Java_com_datadoghq_profiler_JavaProfiler_getSamples(JNIEnv *env,
144157
return (jlong)Profiler::instance()->total_samples();
145158
}
146159

147-
// some duplication between add and remove, though we want to avoid having an extra branch in the hot path
148-
149-
static ThreadFilter::SlotID ensureCurrentThreadFilterSlot(
150-
ThreadFilter *thread_filter, ProfiledThread *current) {
151-
int tid = current->tid();
152-
if (unlikely(tid < 0)) {
153-
return -1;
154-
}
155-
156-
ThreadFilter::SlotID slot_id = current->filterSlotId();
157-
if (likely(slot_id >= 0)) {
158-
if (likely(thread_filter->activeSlotForId(slot_id, tid) != nullptr)) {
159-
return slot_id;
160-
}
161-
current->setFilterSlotId(-1);
162-
}
163-
164-
// Startup can register this TID centrally, but it cannot update another
165-
// pthread's TLS. registerThread(tid) reuses that existing slot.
166-
slot_id = thread_filter->registerThread(tid);
167-
if (slot_id >= 0) {
168-
current->setFilterSlotId(slot_id);
169-
}
170-
return slot_id;
171-
}
172-
173160
// JavaCritical is faster JNI, but more restrictive - parameters and return value have to be
174161
// primitives or arrays of primitive types.
175162
// We direct corresponding JNI calls to JavaCritical to make sure the parameters/return value
@@ -191,7 +178,7 @@ JavaCritical_com_datadoghq_profiler_JavaProfiler_filterThreadAdd0() {
191178
return;
192179
}
193180

194-
int slot_id = ensureCurrentThreadFilterSlot(thread_filter, current);
181+
int slot_id = thread_filter->ensureCurrentThreadSlot(current);
195182
if (unlikely(slot_id < 0)) {
196183
return; // Failed to register thread
197184
}
@@ -385,7 +372,7 @@ Java_com_datadoghq_profiler_JavaProfiler_parkEnter0(
385372
ThreadFilter *tf = profiler->threadFilter();
386373
if (context.spanId == 0 && tf->registryActive() &&
387374
(profiler->taskBlockEnabled() || tf->enabled())) {
388-
ThreadFilter::SlotID slot_id = ensureCurrentThreadFilterSlot(tf, current);
375+
ThreadFilter::SlotID slot_id = tf->ensureCurrentThreadSlot(current);
389376
if (slot_id >= 0) {
390377
current->setParkBlockToken(tf->enterBlockedRun(
391378
slot_id, OSThreadState::CONDVAR_WAIT, BlockRunOwner::JAVA));
@@ -412,32 +399,10 @@ Java_com_datadoghq_profiler_JavaProfiler_parkExit0(
412399
return;
413400
}
414401
Profiler *profiler = Profiler::instance();
415-
bool recording_enabled = profiler->taskBlockEnabled();
416-
bool activity = profiler->tryEnterTaskBlockActivity();
417-
if (!activity) profiler->waitForTaskBlockRotation();
418-
419-
ThreadFilter *tf = profiler->threadFilter();
420-
ThreadFilter::SlotID slot_id = ThreadFilter::tokenSlotId(park_block_token);
421-
ThreadFilter::SlotID current_slot = current->filterSlotId();
422-
if (current_slot < 0) current_slot = tf->slotIdByTid(current->tid());
423-
BlockRunSnapshot snapshot{};
424-
bool exited = current_slot == slot_id &&
425-
tf->snapshotAndExitBlockedRun(
426-
slot_id, ThreadFilter::tokenGeneration(park_block_token), &snapshot);
427-
428-
if (!activity) {
429-
Counters::increment(TASK_BLOCK_DROPPED_ROTATION);
430-
return;
431-
}
432-
if (recording_enabled && exited && snapshot.context_eligible) {
433-
recordTaskBlockIfEligible(
434-
current->tid(), thread, 1, start_ticks, TSC::ticks(), context,
435-
static_cast<u64>(blocker), static_cast<u64>(unblockingSpanId),
436-
snapshot.active_state, true);
437-
} else if (recording_enabled && exited && !snapshot.context_eligible) {
438-
Counters::increment(TASK_BLOCK_SKIPPED_TRACE_CONTEXT);
439-
}
440-
profiler->leaveTaskBlockActivity();
402+
finishTaskBlockAtExit(
403+
current, profiler->threadFilter(), thread, 1, park_block_token,
404+
start_ticks, context, static_cast<u64>(blocker),
405+
static_cast<u64>(unblockingSpanId));
441406
}
442407

443408
static bool decodeJavaBlockState(jint state, OSThreadState &decoded) {
@@ -469,7 +434,7 @@ Java_com_datadoghq_profiler_JavaProfiler_blockEnter0(
469434
if (!profiler->taskBlockEnabled() && !tf->enabled()) {
470435
return 0;
471436
}
472-
ThreadFilter::SlotID slot_id = ensureCurrentThreadFilterSlot(tf, current);
437+
ThreadFilter::SlotID slot_id = tf->ensureCurrentThreadSlot(current);
473438
if (slot_id < 0) return 0;
474439
return static_cast<jlong>(tf->enterBlockedRun(slot_id, decoded));
475440
}
@@ -510,7 +475,7 @@ Java_com_datadoghq_profiler_JavaProfiler_beginTaskBlock0(
510475
}
511476
ThreadFilter *tf = profiler->threadFilter();
512477
if (!tf->unfilteredWallTrackingActive()) return 0;
513-
ThreadFilter::SlotID slot_id = ensureCurrentThreadFilterSlot(tf, current);
478+
ThreadFilter::SlotID slot_id = tf->ensureCurrentThreadSlot(current);
514479
if (slot_id < 0) return 0;
515480

516481
Context context = ContextApi::snapshot();

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

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "jvmThread.h"
1717
#include "libraries.h"
1818
#include "log.h"
19+
#include "mutex.h"
1920
#include "os.h"
2021
#include "profiler.h"
2122
#include "safeAccess.h"
@@ -55,6 +56,12 @@ bool VM::_monitor_events_delegated = false;
5556
bool VM::_native_monitor_events_available = false;
5657
bool VM::_is_adaptive_gc_boundary_flag_set = false;
5758

59+
// Serializes the one-time bridge installation and ownership negotiation.
60+
// Callback readers need no synchronization because ownership is assigned
61+
// before callbacks can be enabled and is never changed afterward.
62+
static Mutex profiler_bridge_init_lock;
63+
static bool profiler_bridge_initialized = false;
64+
5865
jvmtiExtensionFunction VM::_request_stack_trace = nullptr;
5966
jvmtiExtensionFunction VM::_init_request_stack_trace = nullptr;
6067

@@ -86,7 +93,7 @@ static void monitorBlockEnter(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread,
8693
!JVMSupport::isPlatformThread(jni, thread)) {
8794
return;
8895
}
89-
ProfiledThread *current = ProfiledThread::current();
96+
ProfiledThread *current = ProfiledThread::initCurrentThreadSignalSafe();
9097
if (current == nullptr) return;
9198
Context context = ContextApi::snapshot();
9299
if (context.spanId != 0) {
@@ -101,10 +108,15 @@ static void monitorBlockEnter(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread,
101108
bool current_owner = false;
102109
if (token != 0) {
103110
ThreadFilter::SlotID slot_id = ThreadFilter::tokenSlotId(token);
104-
BlockRunSnapshot snapshot = tf->snapshotBlockedRun(slot_id);
105-
current_owner = current->filterSlotId() == slot_id && snapshot.active &&
106-
snapshot.owner == BlockRunOwner::JVMTI &&
107-
snapshot.generation == ThreadFilter::tokenGeneration(token);
111+
ThreadFilter::Slot *slot = current->filterSlotId() == slot_id
112+
? tf->activeSlotForId(slot_id, current->tid())
113+
: nullptr;
114+
if (slot != nullptr) {
115+
BlockRunSnapshot snapshot = slot->snapshotBlockRun();
116+
current_owner = snapshot.active &&
117+
snapshot.owner == BlockRunOwner::JVMTI &&
118+
snapshot.generation == ThreadFilter::tokenGeneration(token);
119+
}
108120
}
109121
if (current_owner) {
110122
return;
@@ -117,12 +129,8 @@ static void monitorBlockEnter(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread,
117129
}
118130

119131
ThreadFilter *tf = profiler->threadFilter();
120-
ThreadFilter::SlotID slot_id = current->filterSlotId();
121-
if (slot_id < 0) {
122-
slot_id = tf->slotIdByTid(current->tid());
123-
if (slot_id >= 0) current->setFilterSlotId(slot_id);
124-
}
125-
if (!tf->allThreads() || slot_id < 0) {
132+
ThreadFilter::SlotID slot_id = tf->ensureCurrentThreadSlot(current);
133+
if (!tf->unfilteredWallTrackingActive() || slot_id < 0) {
126134
current->clearMonitorBlock();
127135
return;
128136
}
@@ -154,31 +162,8 @@ static void monitorBlockExit(JNIEnv *jni, jthread thread, OSThreadState state) {
154162
}
155163

156164
Profiler *profiler = Profiler::instance();
157-
bool recording_enabled = profiler->taskBlockEnabled();
158-
bool activity = profiler->tryEnterTaskBlockActivity();
159-
if (!activity) profiler->waitForTaskBlockRotation();
160-
161-
ThreadFilter *tf = profiler->threadFilter();
162-
ThreadFilter::SlotID slot_id = ThreadFilter::tokenSlotId(token);
163-
ThreadFilter::SlotID current_slot = current->filterSlotId();
164-
if (current_slot < 0) current_slot = tf->slotIdByTid(current->tid());
165-
BlockRunSnapshot snapshot{};
166-
bool exited = current_slot == slot_id &&
167-
tf->snapshotAndExitBlockedRun(
168-
slot_id, ThreadFilter::tokenGeneration(token), &snapshot);
169-
170-
if (!activity) {
171-
Counters::increment(TASK_BLOCK_DROPPED_ROTATION);
172-
return;
173-
}
174-
if (recording_enabled && exited && snapshot.context_eligible) {
175-
recordTaskBlockIfEligible(current->tid(), thread, 0, start_ticks,
176-
TSC::ticks(), context, blocker, 0,
177-
snapshot.active_state, true);
178-
} else if (recording_enabled && exited && !snapshot.context_eligible) {
179-
Counters::increment(TASK_BLOCK_SKIPPED_TRACE_CONTEXT);
180-
}
181-
profiler->leaveTaskBlockActivity();
165+
finishTaskBlockAtExit(current, profiler->threadFilter(), thread, 0, token,
166+
start_ticks, context, blocker, 0);
182167
}
183168

184169
static void JNICALL MonitorContendedEnter(jvmtiEnv *jvmti, JNIEnv *jni,
@@ -576,16 +561,25 @@ bool VM::initializeRequestStackTrace() {
576561
return false;
577562
}
578563

579-
bool VM::initProfilerBridge(JavaVM *vm, bool attach,
580-
bool delegateMonitorEvents) {
564+
ProfilerBridgeInitResult VM::initProfilerBridge(JavaVM *vm, bool attach,
565+
bool delegateMonitorEvents) {
566+
MutexLocker init_locker(profiler_bridge_init_lock);
567+
if (profiler_bridge_initialized) {
568+
bool requested_delegation =
569+
delegateMonitorEvents && _native_monitor_events_available;
570+
return requested_delegation == _monitor_events_delegated
571+
? ProfilerBridgeInitResult::SUCCESS
572+
: ProfilerBridgeInitResult::MONITOR_EVENTS_DELEGATION_CONFLICT;
573+
}
574+
581575
TEST_LOG("VM::initProfilerBridge");
582576
if (!initShared(vm)) {
583-
return false;
577+
return ProfilerBridgeInitResult::FAILURE;
584578
}
585579

586580
CodeCache *lib = openJvmLibrary();
587581
if (lib == nullptr) {
588-
return false;
582+
return ProfilerBridgeInitResult::FAILURE;
589583
}
590584

591585
if (!attach && hotspot_version() == 8 && OS::isLinux()) {
@@ -708,7 +702,8 @@ bool VM::initProfilerBridge(JavaVM *vm, bool attach,
708702

709703
OS::installSignalHandler(WAKEUP_SIGNAL, NULL, wakeupHandler);
710704

711-
return true;
705+
profiler_bridge_initialized = true;
706+
return ProfilerBridgeInitResult::SUCCESS;
712707
}
713708

714709
bool VM::setNativeMonitorEventsEnabled(bool enabled) {
@@ -859,7 +854,8 @@ Agent_OnLoad(JavaVM* vm, char* options, void* reserved) {
859854
return ARGUMENTS_ERROR;
860855
}
861856

862-
if (!VM::initProfilerBridge(vm, false)) {
857+
if (VM::initProfilerBridge(vm, false) !=
858+
ProfilerBridgeInitResult::SUCCESS) {
863859
Log::error("JVM does not support Tool Interface");
864860
return COMMAND_ERROR;
865861
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@ class JavaVersionAccess {
132132
static int get_hotspot_version(char* prop_value);
133133
};
134134

135+
// The profiler bridge is process-wide and initialized exactly once. Later Java
136+
// API initialization may reuse it only with the same effective Object.wait
137+
// ownership.
138+
enum class ProfilerBridgeInitResult {
139+
SUCCESS,
140+
FAILURE,
141+
MONITOR_EVENTS_DELEGATION_CONFLICT,
142+
};
143+
135144
class VM {
136145
friend class VMTestAccessor;
137146

@@ -179,8 +188,8 @@ class VM {
179188
static JVM_GetManagement _getManagement;
180189

181190
static bool initLibrary(JavaVM *vm);
182-
static bool initProfilerBridge(JavaVM *vm, bool attach,
183-
bool delegateMonitorEvents = false);
191+
static ProfilerBridgeInitResult initProfilerBridge(
192+
JavaVM *vm, bool attach, bool delegateMonitorEvents = false);
184193

185194
static jvmtiEnv *jvmti() { return _jvmti; }
186195

ddprof-lib/src/main/java/com/datadoghq/profiler/JavaProfiler.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ public static synchronized JavaProfiler getInstance(String libLocation, String s
137137
/**
138138
* Get a {@linkplain JavaProfiler} instance with explicit monitor-event ownership.
139139
*
140-
* <p>The first successful initialization fixes this process-wide setting because the native
141-
* profiler is a singleton. When delegation is enabled, Java instrumentation owns
140+
* <p>The first successful native bridge initialization fixes this process-wide setting because
141+
* the native profiler is a singleton. This may occur during {@code -agentpath} startup before
142+
* this method is called. When delegation is enabled, Java instrumentation owns
142143
* {@code Object.wait} TaskBlock intervals and native JVMTI wait callbacks are suppressed;
143144
* native JVMTI callbacks continue to own synchronized monitor contention.
144145
*
@@ -147,6 +148,8 @@ public static synchronized JavaProfiler getInstance(String libLocation, String s
147148
* @param delegateMonitorWaitEvents whether Java instrumentation owns {@code Object.wait} intervals
148149
* @return the process-wide profiler instance
149150
* @throws IOException if the native library cannot be loaded
151+
* @throws IllegalStateException if monitor ownership conflicts with an earlier native bridge
152+
* initialization
150153
*/
151154
public static synchronized JavaProfiler getInstance(String libLocation, String scratchDir,
152155
boolean delegateMonitorWaitEvents) throws IOException {

0 commit comments

Comments
 (0)