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/7] 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/7] 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/7] 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(); From 135a03d26eb75b33a0749acf59f8c1088fac0af4 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 13 Mar 2026 14:13:29 -0400 Subject: [PATCH 4/7] Clean-up - Renamed TraceList -> SpanList - Fix broken test - use SpanList in Groovy test --- .../src/main/java/datadog/trace/core/CoreTracer.java | 10 ++++++---- .../src/main/java/datadog/trace/core/PendingTrace.java | 6 +++--- .../trace/core/{TraceList.java => SpanList.java} | 10 +++++----- .../datadog/trace/core/StreamingTraceCollector.java | 2 +- .../datadog/trace/core/TraceInterceptorTest.groovy | 3 ++- 5 files changed, 17 insertions(+), 14 deletions(-) rename dd-trace-core/src/main/java/datadog/trace/core/{TraceList.java => SpanList.java} (56%) 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 812d60e8b0e..2a474735990 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 @@ -1219,7 +1219,7 @@ public AgentDataStreamsMonitoring getDataStreamsMonitoring() { * * @param trace a list of the spans related to the same trace */ - void write(final TraceList trace) { + void write(final SpanList trace) { if (trace.isEmpty() || !trace.get(0).traceConfig().isTraceEnabled()) { return; } @@ -1256,7 +1256,7 @@ void write(final TraceList trace) { } } - private List interceptCompleteTrace(TraceList originalTrace) { + private List interceptCompleteTrace(SpanList 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 @@ -1285,8 +1285,10 @@ private List interceptCompleteTrace(TraceList originalTrace) { } } - if (interceptedTrace != originalTrace || originalTrace.modCount() != originalModCount) { - TraceList trace = new TraceList(interceptedTrace.size()); + if (interceptedTrace == null || interceptedTrace.isEmpty()) { + return SpanList.EMPTY; + } else if (interceptedTrace != originalTrace || originalTrace.modCount() != originalModCount) { + SpanList trace = new SpanList(interceptedTrace.size()); for (final MutableSpan span : interceptedTrace) { if (span instanceof DDSpan) { trace.add((DDSpan) span); 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 8ed7d62d489..19525d6109b 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 TraceList trace; + final SpanList 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 TraceList(size); + trace = new SpanList(size); completedSpans = enqueueSpansToWrite(trace, writeRunningSpans); } else { - trace = TraceList.EMPTY; + trace = SpanList.EMPTY; } } if (!trace.isEmpty()) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/TraceList.java b/dd-trace-core/src/main/java/datadog/trace/core/SpanList.java similarity index 56% rename from dd-trace-core/src/main/java/datadog/trace/core/TraceList.java rename to dd-trace-core/src/main/java/datadog/trace/core/SpanList.java index 18c7d86bca7..a33376a8867 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/TraceList.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/SpanList.java @@ -3,16 +3,16 @@ 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); +final class SpanList extends ArrayList { + static final SpanList EMPTY = new SpanList(0); - static final TraceList of(DDSpan span) { - TraceList list = new TraceList(1); + static final SpanList of(DDSpan span) { + SpanList list = new SpanList(1); list.add(span); return list; } - TraceList(int capacity) { + SpanList(int capacity) { super(capacity); } 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 43d699fbd04..e886682f05b 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 @@ -71,7 +71,7 @@ PublishState onPublish(DDSpan span) { tracer.onRootSpanPublished(rootSpan); } healthMetrics.onFinishSpan(); - tracer.write(TraceList.of(span)); + tracer.write(SpanList.of(span)); return PublishState.WRITTEN; } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/TraceInterceptorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/TraceInterceptorTest.groovy index e12c62a39b8..5795497acf2 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/TraceInterceptorTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/TraceInterceptorTest.groovy @@ -6,6 +6,7 @@ import datadog.trace.api.config.TracerConfig import datadog.trace.api.interceptor.MutableSpan import datadog.trace.api.interceptor.TraceInterceptor import datadog.trace.common.writer.ListWriter +import datadog.trace.core.SpanList import datadog.trace.core.test.DDCoreSpecification import spock.lang.Timeout @@ -188,7 +189,7 @@ class TraceInterceptorTest extends DDCoreSpecification { when: DDSpan span = (DDSpan) tracer.startSpan("test", "test") span.phasedFinish() - tracer.write([span]) + tracer.write(SpanList.of(span)) then: notThrown(Throwable) From f7094741222c548ec3bd941cee45dfd1fdd9c647 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 13 Mar 2026 15:10:17 -0400 Subject: [PATCH 5/7] Refactor & tests Refactored the code to clean-up the edge case handling, quick return on interceptor empty and trace empty. That reduced the nesting level and made the code easier to read. Also flipped the code that handles unaltered lists, that reads a bit better. Added tests for all these cases to CoreTracerTest2. To make this easier to test in isolation, created a static interceptCompleteTrace that can be called directly by the test methods. --- .../java/datadog/trace/core/CoreTracer.java | 88 +++++++------ .../datadog/trace/core/CoreTracerTest2.java | 117 ++++++++++++++++++ 2 files changed, 165 insertions(+), 40 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 2a474735990..be0c4a330f3 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 @@ -1211,7 +1211,7 @@ public AgentDataStreamsMonitoring getDataStreamsMonitoring() { return dataStreamsMonitoring; } - private final RatelimitedLogger rlLog = new RatelimitedLogger(log, 1, MINUTES); + private static final RatelimitedLogger rlLog = new RatelimitedLogger(log, 1, MINUTES); /** * We use the sampler to know if the trace has to be reported/written. The sampler is called on @@ -1257,49 +1257,57 @@ void write(final SpanList trace) { } private List interceptCompleteTrace(SpanList 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 - interceptedTrace = interceptor.onTraceComplete(interceptedTrace); - } catch (Throwable e) { - String interceptorName = interceptor.getClass().getName(); - rlLog.warn("Throwable raised in TraceInterceptor {}", interceptorName, e); - } - if (interceptedTrace == null) { - interceptedTrace = emptyList(); - } + return interceptCompleteTrace(interceptors, originalTrace); + } + + static final List interceptCompleteTrace( + SortedSet interceptors, SpanList originalTrace) { + if (interceptors.isEmpty()) { + return originalTrace; + } + if (originalTrace.isEmpty()) { + return SpanList.EMPTY; + } + + // 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 + interceptedTrace = interceptor.onTraceComplete(interceptedTrace); + } catch (Throwable e) { + String interceptorName = interceptor.getClass().getName(); + rlLog.warn("Throwable raised in TraceInterceptor {}", interceptorName, e); + } + if (interceptedTrace == null) { + interceptedTrace = emptyList(); } + } - if (interceptedTrace == null || interceptedTrace.isEmpty()) { - return SpanList.EMPTY; - } else if (interceptedTrace != originalTrace || originalTrace.modCount() != originalModCount) { - SpanList trace = new SpanList(interceptedTrace.size()); - for (final MutableSpan span : interceptedTrace) { - if (span instanceof DDSpan) { - trace.add((DDSpan) span); - } + if (interceptedTrace == null || interceptedTrace.isEmpty()) { + return SpanList.EMPTY; + } else if (interceptedTrace == originalTrace && originalTrace.modCount() == originalModCount) { + return originalTrace; + } else { + SpanList trace = new SpanList(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; } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest2.java b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest2.java index b613c92a21e..c1da3611ebb 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest2.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest2.java @@ -8,10 +8,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import datadog.trace.api.Config; +import datadog.trace.api.interceptor.MutableSpan; +import datadog.trace.api.interceptor.TraceInterceptor; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.core.CoreTracer.CoreSpanBuilder; import datadog.trace.core.CoreTracer.ReusableSingleSpanBuilder; import datadog.trace.core.CoreTracer.ReusableSingleSpanBuilderThreadLocalCache; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; import org.junit.jupiter.api.Test; // named CoreTracerTest2 to avoid collision with Groovy which appears to have messed up test @@ -179,4 +186,114 @@ public void start_not_inUse() { ReusableSingleSpanBuilder builder = new ReusableSingleSpanBuilder(TRACER); assertThrows(AssertionError.class, () -> builder.start()); } + + @Test + public void noInterceptorsTest() { + // No interceptors should return the original list to avoid allocation + DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo"); + + SpanList list = SpanList.of(span); + List interceptedList = + CoreTracer.interceptCompleteTrace(Collections.emptySortedSet(), list); + assertSame(list, interceptedList); + } + + @Test + public void interceptNoInterceptors() { + // No interceptors should return the original list to avoid allocation + DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo"); + + SpanList list = SpanList.of(span); + List interceptedList = + CoreTracer.interceptCompleteTrace(Collections.emptySortedSet(), list); + assertSame(list, interceptedList); + } + + @Test + public void interceptEmptyList() { + DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo"); + SortedSet interceptors = interceptors((list) -> SpanList.of(span)); + + SpanList list = new SpanList(0); // not using EMPTY deliberately + List interceptedList = CoreTracer.interceptCompleteTrace(interceptors, list); + assertTrue(interceptedList.isEmpty()); + } + + @Test + public void interceptUnchanged() { + SortedSet interceptors = interceptors((list) -> list, (list) -> list); + + DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo"); + SpanList list = SpanList.of(span); + List interceptedList = CoreTracer.interceptCompleteTrace(interceptors, list); + assertSame(list, interceptedList); + } + + @Test + public void interceptNewList() { + DDSpan substituteSpan = (DDSpan) TRACER.startSpan("sub", "sub"); + SpanList substituteList = SpanList.of(substituteSpan); + SortedSet interceptors = + interceptors((list) -> list, (list) -> substituteList); + + DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo"); + SpanList list = SpanList.of(span); + List interceptedList = CoreTracer.interceptCompleteTrace(interceptors, list); + assertEquals(1, interceptedList.size()); + assertEquals("sub", interceptedList.get(0).getOperationName()); + } + + @Test + public void interceptAlteredList() { + // This is an unlikely case and arguably not something we need to support + + DDSpan substituteSpan = (DDSpan) TRACER.startSpan("sub", "sub"); + SortedSet interceptors = + interceptors( + (list) -> list, + (list) -> { + List erasedList = (List) list; + erasedList.clear(); + erasedList.add(substituteSpan); + return erasedList; + }); + + DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo"); + SpanList list = SpanList.of(span); + List interceptedList = CoreTracer.interceptCompleteTrace(interceptors, list); + assertNotSame(interceptedList, list); + assertEquals(1, interceptedList.size()); + assertEquals("sub", interceptedList.get(0).getOperationName()); + } + + static final SortedSet interceptors(TestInterceptor... interceptors) { + TreeSet interceptorSet = + new TreeSet<>((lhs, rhs) -> Integer.compare(lhs.priority(), rhs.priority())); + for (int i = 0; i < interceptors.length; ++i) { + int priority = i; + TestInterceptor interceptor = interceptors[i]; + + interceptorSet.add( + new TraceInterceptor() { + @Override + public int priority() { + return priority; + } + + @Override + public Collection onTraceComplete( + Collection trace) { + return interceptor.onTraceComplete(trace); + } + }); + } + return interceptorSet; + } + + // Matches TraceInterceptor but priority is implied in interceptors + // Only having onTraceComplete allows this to @FunctionalInterface and a little nicer for a test + @FunctionalInterface + interface TestInterceptor { + Collection onTraceComplete(Collection trace); + } } From 58cdbbfc83f209f0d723582f0fd6b9840b229e01 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 17 Mar 2026 12:58:00 -0400 Subject: [PATCH 6/7] Adding some Javadoc --- .../src/main/java/datadog/trace/core/SpanList.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/SpanList.java b/dd-trace-core/src/main/java/datadog/trace/core/SpanList.java index a33376a8867..2ce199b1768 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/SpanList.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/SpanList.java @@ -6,16 +6,29 @@ final class SpanList extends ArrayList { static final SpanList EMPTY = new SpanList(0); + /** + * Convenience function for creating a SpanList containing a single DDSpan. Meant as replacement + * for Collections.singletonList when creating a SpanList. + * + * @param span != null + * @return a SpanList + */ static final SpanList of(DDSpan span) { SpanList list = new SpanList(1); list.add(span); return list; } + /** + * Constructs a SpanList with the specified capacity + * + * @param capacity + */ SpanList(int capacity) { super(capacity); } + /** The modifcation count of the List - can be used to check if the List has been altered */ int modCount() { return this.modCount; } From 456e9ee24d2a7f9cf89020ee9c28f9d2f6a81b99 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 17 Mar 2026 16:30:04 -0400 Subject: [PATCH 7/7] codeNarc - removing unnecessary import --- .../test/groovy/datadog/trace/core/TraceInterceptorTest.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/TraceInterceptorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/TraceInterceptorTest.groovy index 5795497acf2..0b63a79aa36 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/TraceInterceptorTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/TraceInterceptorTest.groovy @@ -6,7 +6,6 @@ import datadog.trace.api.config.TracerConfig import datadog.trace.api.interceptor.MutableSpan import datadog.trace.api.interceptor.TraceInterceptor import datadog.trace.common.writer.ListWriter -import datadog.trace.core.SpanList import datadog.trace.core.test.DDCoreSpecification import spock.lang.Timeout