Skip to content

Commit 9e18cc4

Browse files
[mq] [skip ddci] working branch - merge 4bae9f2 on top of master at 7b903a5
{"baseBranch":"master","baseCommit":"7b903a53644abc39f55b2fb21283546ae9801f35","createdAt":"2026-09-21T07:18:30.910620Z","headSha":"4bae9f2b0c2b12f853023cdd125f7f30419115cd","id":"edaf4b06-c80e-403d-8aaa-ead54310d0da","priority":"200","pullRequestNumber":"12550","queuedAt":"2026-09-21T07:18:30.908989Z","status":"STATUS_QUEUED"}
2 parents 9609bf4 + 4bae9f2 commit 9e18cc4

3 files changed

Lines changed: 121 additions & 8 deletions

File tree

‎dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/QueueTimerHelper.java‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ private static final class RateLimiterHolder {
2929
ProfilingConfig.PROFILING_UPLOAD_PERIOD_DEFAULT)));
3030
}
3131

32+
/** Whether queue timing can safely start. */
33+
public static boolean isReady() {
34+
// Queue timing is unsupported in native images. JFR must initialise the TSC frequency first.
35+
return !Platform.isNativeImage() && InstrumentationBasedProfiling.isJFRReady();
36+
}
37+
3238
public static <T> void startQueuingTimer(
3339
ContextStore<T, State> taskContextStore,
3440
Class<?> schedulerClass,
@@ -41,14 +47,8 @@ public static <T> void startQueuingTimer(
4147

4248
public static void startQueuingTimer(
4349
State state, Class<?> schedulerClass, Class<?> queueClass, int queueLength, Object task) {
44-
if (Platform.isNativeImage()) {
45-
// explicitly not supported for Graal native image
46-
return;
47-
}
4850
// TODO consider queue length based sampling here to reduce overhead
49-
// avoid calling this before JFR is initialised because it will lead to reading the wrong
50-
// TSC frequency before JFR has set it up properly
51-
if (task != null && state != null && InstrumentationBasedProfiling.isJFRReady()) {
51+
if (task != null && state != null && isReady()) {
5252
QueueTiming timing =
5353
(QueueTiming) AgentTracer.get().getProfilingContext().start(Timer.TimerType.QUEUEING);
5454
timing.setTask(task);

‎dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/executor/ThreadPoolExecutorInstrumentation.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public static void capture(
113113
// queue time needs to be handled separately because there are RunnableFutures which are
114114
// excluded as
115115
// Runnables but it is not until now that they will be put on the executor's queue
116-
if (!exclude(EXECUTOR, tpe)) {
116+
if (QueueTimerHelper.isReady() && !exclude(EXECUTOR, tpe)) {
117117
if (!exclude(RUNNABLE, task)) {
118118
Queue<?> queue = tpe.getQueue();
119119
QueueTimerHelper.startQueuingTimer(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import datadog.trace.agent.test.InstrumentationSpecification
2+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
3+
import datadog.trace.bootstrap.instrumentation.jfr.InstrumentationBasedProfiling
4+
5+
import java.util.concurrent.CountDownLatch
6+
import java.util.concurrent.LinkedBlockingQueue
7+
import java.util.concurrent.ThreadPoolExecutor
8+
import java.util.concurrent.TimeUnit
9+
import java.util.concurrent.atomic.AtomicInteger
10+
11+
import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
12+
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
13+
14+
class QueueTimingReadinessForkedTest extends InstrumentationSpecification {
15+
@Override
16+
protected void configurePreAgent() {
17+
injectSysConfig("dd.profiling.enabled", "true")
18+
injectSysConfig("dd.profiling.queueing.time.enabled", "true")
19+
super.configurePreAgent()
20+
}
21+
22+
def "queue metadata is read only after JFR is ready while tracing always propagates"() {
23+
setup:
24+
def queue = new CountingQueue()
25+
def executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, queue)
26+
assert !InstrumentationBasedProfiling.isJFRReady()
27+
28+
when:
29+
runTasks(executor, "before")
30+
31+
then:
32+
queue.sizeCalls.get() == 0
33+
TEST_PROFILING_CONTEXT_INTEGRATION.closedTimings.isEmpty()
34+
assertTraces(2) {
35+
trace(1) {
36+
basicSpan(it, "execute-before")
37+
}
38+
trace(1) {
39+
basicSpan(it, "submit-before")
40+
}
41+
}
42+
43+
when:
44+
TEST_WRITER.clear()
45+
InstrumentationBasedProfiling.enableInstrumentationBasedProfiling()
46+
runTasks(executor, "after")
47+
48+
then:
49+
queue.sizeCalls.get() == 2
50+
assertTraces(2) {
51+
trace(1) {
52+
basicSpan(it, "execute-after")
53+
}
54+
trace(1) {
55+
basicSpan(it, "submit-after")
56+
}
57+
}
58+
TEST_PROFILING_CONTEXT_INTEGRATION.isBalanced()
59+
TEST_PROFILING_CONTEXT_INTEGRATION.closedTimings.size() == 2
60+
TEST_PROFILING_CONTEXT_INTEGRATION.closedTimings.every {
61+
it.queue == CountingQueue && it.scheduler == ThreadPoolExecutor && it.queueLength >= 0
62+
}
63+
64+
cleanup:
65+
executor.shutdownNow()
66+
assert executor.awaitTermination(5, TimeUnit.SECONDS)
67+
TEST_PROFILING_CONTEXT_INTEGRATION.closedTimings.clear()
68+
}
69+
70+
private static void runTasks(ThreadPoolExecutor executor, String phase) {
71+
runUnderTrace("execute-" + phase) {
72+
def expectedSpanId = AgentTracer.activeSpan().spanId
73+
def task = new ContextCheckingTask()
74+
executor.execute(task)
75+
assert task.done.await(5, TimeUnit.SECONDS)
76+
assert task.spanId == expectedSpanId
77+
}
78+
runUnderTrace("submit-" + phase) {
79+
def expectedSpanId = AgentTracer.activeSpan().spanId
80+
def task = new ContextCheckingTask()
81+
executor.submit(task).get(5, TimeUnit.SECONDS)
82+
assert task.spanId == expectedSpanId
83+
}
84+
}
85+
86+
static class ContextCheckingTask implements Runnable {
87+
final CountDownLatch done = new CountDownLatch(1)
88+
volatile long spanId
89+
90+
@Override
91+
void run() {
92+
def span = AgentTracer.activeSpan()
93+
spanId = span == null ? 0 : span.spanId
94+
done.countDown()
95+
}
96+
}
97+
98+
static class CountingQueue extends LinkedBlockingQueue<Runnable> {
99+
final AtomicInteger sizeCalls = new AtomicInteger()
100+
101+
@Override
102+
int size() {
103+
sizeCalls.incrementAndGet()
104+
return super.size()
105+
}
106+
107+
@Override
108+
boolean isEmpty() {
109+
// Executor housekeeping must not count as a queue-timing metadata read.
110+
return super.size() == 0
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)