From 8a33be45bf50894f7e4808c6371d15addf2706da Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 10 Mar 2026 14:57:52 -0400 Subject: [PATCH 1/3] Skip copying to another array if interceptedTrace == trace --- .../src/main/java/datadog/trace/core/CoreTracer.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index c58d1ff277d..1c34b24e8fb 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -1255,10 +1255,14 @@ private List interceptCompleteTrace(List trace) { } } - trace = new ArrayList<>(interceptedTrace.size()); - for (final MutableSpan span : interceptedTrace) { - if (span instanceof DDSpan) { - trace.add((DDSpan) span); + // DQH - common case is that the list is that we return the same list (usually unaltered) + // In that case, there's no need to copy into a new list + if ( interceptedTrace != trace ) { + trace = new ArrayList<>(interceptedTrace.size()); + for (final MutableSpan span : interceptedTrace) { + if (span instanceof DDSpan) { + trace.add((DDSpan) span); + } } } } From e092c44de437fa23b9399a07be04f99edd70a5b7 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 12 Mar 2026 15:00:55 -0400 Subject: [PATCH 2/3] Reducing allocation in interceptCompleteTrace Introduced TraceList which is just an ArrayList that exposes the internal modCount Being able to access the modCount allows interceptCompleteTrace to check if the list post-interception has been modified If the list is the same & is unmodified, then interceptCompleteTrace is able to bypass making a defensive copy As a further optimization, use of TraceList is pushed up to callers of interceptCompleteTrace and TraceCollector.write That does mean StreamingTraceCollector can no longer used a singletonList, but the savings throughput the pipeline outweigh the cost of the extra Object[] in that case --- .../java/datadog/trace/core/CoreTracer.java | 33 ++++++++++++++----- .../java/datadog/trace/core/PendingTrace.java | 6 ++-- .../trace/core/StreamingTraceCollector.java | 3 +- .../java/datadog/trace/core/TraceList.java | 22 +++++++++++++ 4 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 dd-trace-core/src/main/java/datadog/trace/core/TraceList.java diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 1c34b24e8fb..53bb5c2e534 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -1202,7 +1202,7 @@ public AgentDataStreamsMonitoring getDataStreamsMonitoring() { * * @param trace a list of the spans related to the same trace */ - void write(final List trace) { + void write(final TraceList trace) { if (trace.isEmpty() || !trace.get(0).traceConfig().isTraceEnabled()) { return; } @@ -1239,9 +1239,22 @@ void write(final List trace) { } } - private List interceptCompleteTrace(List trace) { - if (!interceptors.isEmpty() && !trace.isEmpty()) { - Collection interceptedTrace = new ArrayList<>(trace); + private List interceptCompleteTrace(TraceList originalTrace) { + if (!interceptors.isEmpty() && !originalTrace.isEmpty()) { + // Using TraceList to optimize the common case where the interceptors, + // don't alter the list. If the interceptors just return the provided + // List, then no need to copy to another List. + + // As an extra precaution, also check the modCount before and after on + // the TraceList, since TraceInterceptor could put some other type of + // object into the List. + + // There is still a risk that a TraceInterceptor holds onto the provided + // List and modifies it later on, but we cannot safeguard against + // every possible misuse. + Collection interceptedTrace = originalTrace; + int originalModCount = originalTrace.modCount(); + for (final TraceInterceptor interceptor : interceptors) { try { // If one TraceInterceptor throws an exception, then continue with the next one @@ -1255,18 +1268,20 @@ private List interceptCompleteTrace(List trace) { } } - // DQH - common case is that the list is that we return the same list (usually unaltered) - // In that case, there's no need to copy into a new list - if ( interceptedTrace != trace ) { - trace = new ArrayList<>(interceptedTrace.size()); + if (interceptedTrace != originalTrace || originalTrace.modCount() != originalModCount) { + TraceList trace = new TraceList(interceptedTrace.size()); for (final MutableSpan span : interceptedTrace) { if (span instanceof DDSpan) { trace.add((DDSpan) span); } } + return trace; + } else { + return originalTrace; } + } else { + return originalTrace; } - return trace; } @Override diff --git a/dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java b/dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java index 4842a53c2ae..8ed7d62d489 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java @@ -327,7 +327,7 @@ private int write(boolean isPartial) { if (!spans.isEmpty()) { try (Recording recording = tracer.writeTimer()) { // Only one writer at a time - final List trace; + final TraceList trace; int completedSpans = 0; synchronized (this) { if (!isPartial) { @@ -346,10 +346,10 @@ private int write(boolean isPartial) { // count(s) will be incremented, and any new spans added during the period that the count // was negative will be written by someone even if we don't write them right now. if (size > 0 && (!isPartial || size >= tracer.getPartialFlushMinSpans())) { - trace = new ArrayList<>(size); + trace = new TraceList(size); completedSpans = enqueueSpansToWrite(trace, writeRunningSpans); } else { - trace = EMPTY; + trace = TraceList.EMPTY; } } if (!trace.isEmpty()) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/StreamingTraceCollector.java b/dd-trace-core/src/main/java/datadog/trace/core/StreamingTraceCollector.java index 366fde4f897..43d699fbd04 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/StreamingTraceCollector.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/StreamingTraceCollector.java @@ -4,7 +4,6 @@ import datadog.trace.api.time.TimeSource; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.core.monitor.HealthMetrics; -import java.util.Collections; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import javax.annotation.Nonnull; @@ -72,7 +71,7 @@ PublishState onPublish(DDSpan span) { tracer.onRootSpanPublished(rootSpan); } healthMetrics.onFinishSpan(); - tracer.write(Collections.singletonList(span)); + tracer.write(TraceList.of(span)); return PublishState.WRITTEN; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/TraceList.java b/dd-trace-core/src/main/java/datadog/trace/core/TraceList.java new file mode 100644 index 00000000000..18c7d86bca7 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/TraceList.java @@ -0,0 +1,22 @@ +package datadog.trace.core; + +import java.util.ArrayList; + +/** ArrayList that exposes modCount to allow for an optimization in TraceInterceptor handling */ +final class TraceList extends ArrayList { + static final TraceList EMPTY = new TraceList(0); + + static final TraceList of(DDSpan span) { + TraceList list = new TraceList(1); + list.add(span); + return list; + } + + TraceList(int capacity) { + super(capacity); + } + + int modCount() { + return this.modCount; + } +} From 139625fedb702ebbe7375f7ae1394fd7956950d6 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 12 Mar 2026 16:21:10 -0400 Subject: [PATCH 3/3] spotless --- dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 53bb5c2e534..e13329f0f7b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -1250,7 +1250,7 @@ private List interceptCompleteTrace(TraceList originalTrace) { // object into the List. // There is still a risk that a TraceInterceptor holds onto the provided - // List and modifies it later on, but we cannot safeguard against + // List and modifies it later on, but we cannot safeguard against // every possible misuse. Collection interceptedTrace = originalTrace; int originalModCount = originalTrace.modCount();