Skip to content

Commit ad5299c

Browse files
committed
fix: emit Thread.sleep TaskBlocks for never-attached threads
1 parent 310bc72 commit ad5299c

4 files changed

Lines changed: 50 additions & 9 deletions

File tree

dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingIntegration.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ public class DatadogProfilingIntegration implements ProfilingContextIntegration
3434

3535
// --- Async TaskBlock recording infrastructure (Thread.sleep deferred path) ---
3636

37-
// Pre-cached native tid per traced thread; populated in onAttach() to avoid repeated JNI on
38-
// the hot path. Reading a ThreadLocal is pure Java once the value is set.
37+
// Pre-cached native tid per thread. Traced threads populate this in onAttach(); spanless
38+
// Thread.sleep instrumentation lazily resolves it without calling onAttach(), because onAttach()
39+
// also changes wall-clock thread filtering state.
3940
private static final ThreadLocal<Integer> NATIVE_TID = new ThreadLocal<>();
4041

4142
private static final BigInteger NANOS_PER_SECOND = BigInteger.valueOf(1_000_000_000L);
@@ -109,6 +110,18 @@ public int getCurrentThreadId() {
109110
return tid != null ? tid : -1;
110111
}
111112

113+
private static int getOrResolveCurrentThreadId() {
114+
Integer tid = NATIVE_TID.get();
115+
if (tid != null) {
116+
return tid;
117+
}
118+
int resolvedTid = DDPROF.getCurrentThreadId();
119+
if (resolvedTid >= 0) {
120+
NATIVE_TID.set(resolvedTid);
121+
}
122+
return resolvedTid;
123+
}
124+
112125
@Override
113126
public long blockEnter(int state) {
114127
return DDPROF.blockEnter(state);
@@ -143,7 +156,7 @@ public void enqueueTaskBlock(
143156
if (spanId != 0L) {
144157
return;
145158
}
146-
int tid = getCurrentThreadId();
159+
int tid = getOrResolveCurrentThreadId();
147160
if (tid < 0) {
148161
return;
149162
}

dd-java-agent/instrumentation/datadog/profiling/thread-sleep/src/main/java/datadog/trace/instrumentation/threadsleep/ThreadSleepProfilingInstrumentation.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
* Caller-site rewriting is the Java-level way to bracket direct {@code Thread.sleep} call sites
2626
* with a TaskBlock interval.
2727
*
28-
* <p>Coverage is purely opt-in by the user's bytecode: any supported direct {@code
29-
* Thread.sleep(...)} call site in a non-JDK class is wrapped. {@code TimeUnit.sleep(long)} is
30-
* instrumented separately at the bootstrap method boundary. Reflection-driven sleeps and JNI-driven
31-
* sleeps remain uncovered (intentional: out-of-band call paths).
28+
* <p>Coverage is purely opt-in by the user's bytecode: supported direct {@code Thread.sleep(...)}
29+
* call sites in non-excluded classes are wrapped. {@code TimeUnit.sleep(long)} is instrumented
30+
* separately at the bootstrap method boundary. Reflection-driven sleeps and JNI-driven sleeps
31+
* remain uncovered (intentional: out-of-band call paths).
3232
*
3333
* <p>Active on every JDK when enabled via {@code profiling.ddprof.wall.precheck=true} (opt-in;
3434
* default is off). The native JVMTI monitor callbacks cover {@code Object.wait()} and synchronized

dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/ThreadSleepTaskBlockForkedApp.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public static void main(String[] args) throws Exception {
1818
app.runSubThresholdSleeps();
1919
Thread.currentThread().setName("threadsleep-spanless");
2020
app.runSpanlessSleeps();
21+
app.runNeverAttachedSpanlessSleeps();
2122
Thread.sleep(1500);
2223
}
2324

@@ -44,6 +45,23 @@ private void runSpanlessSleeps() throws InterruptedException {
4445
}
4546
}
4647

48+
private void runNeverAttachedSpanlessSleeps() throws InterruptedException {
49+
Thread thread =
50+
new Thread(
51+
() -> {
52+
try {
53+
for (int i = 0; i < SLEEP_ITERATIONS; i++) {
54+
Thread.sleep(LONG_SLEEP_MILLIS);
55+
}
56+
} catch (InterruptedException e) {
57+
Thread.currentThread().interrupt();
58+
}
59+
},
60+
"threadsleep-never-attached");
61+
thread.start();
62+
thread.join();
63+
}
64+
4765
private void runSubThresholdSleeps() throws InterruptedException {
4866
for (int i = 0; i < SLEEP_ITERATIONS; i++) {
4967
Span span = tracer.buildSpan("threadsleep.short").start();

dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/ThreadSleepTaskBlockProfilingTest.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ void threadSleepEmitsTaskBlockEvents() throws Exception {
9191
assertTrue(
9292
stats.spanlessTaskBlockCount > 0,
9393
"Expected datadog.TaskBlock events from spanless Thread.sleep call sites");
94+
assertTrue(
95+
stats.neverAttachedTaskBlockCount > 0,
96+
"Expected datadog.TaskBlock events from never-attached spanless Thread.sleep call sites");
9497
assertFalse(stats.hasActiveSpanTaskBlock, "Active-span sleeps must not emit TaskBlock events");
9598
assertFalse(stats.hasNonZeroSpanId, "Spanless TaskBlock events must carry zero spanId");
9699
assertFalse(
@@ -243,6 +246,7 @@ private boolean logHasInstrumentationError() throws IOException {
243246
/** Aggregate counts/flags collected across all JFR streams produced by the forked process. */
244247
private static final class JfrStats {
245248
long spanlessTaskBlockCount;
249+
long neverAttachedTaskBlockCount;
246250
boolean hasActiveSpanTaskBlock;
247251
boolean hasNonZeroSpanId;
248252
boolean hasNonZeroLocalRootSpanId;
@@ -264,12 +268,18 @@ void add(IItemCollection events) {
264268
hasActiveSpanTaskBlock = true;
265269
continue;
266270
}
267-
if (!"threadsleep-spanless".equals(threadName)) {
271+
boolean spanlessThread = "threadsleep-spanless".equals(threadName);
272+
boolean neverAttachedThread = "threadsleep-never-attached".equals(threadName);
273+
if (!spanlessThread && !neverAttachedThread) {
268274
continue;
269275
}
270276
long spanId = span.getMember(item).longValue();
271277
long rootSpanId = root.getMember(item).longValue();
272-
spanlessTaskBlockCount++;
278+
if (spanlessThread) {
279+
spanlessTaskBlockCount++;
280+
} else {
281+
neverAttachedTaskBlockCount++;
282+
}
273283
hasNonZeroSpanId |= spanId != 0L;
274284
hasNonZeroLocalRootSpanId |= rootSpanId != 0L;
275285
hasMissingEventThread |= threadName == null || threadName.isEmpty();

0 commit comments

Comments
 (0)