Skip to content

Commit 470e304

Browse files
dougqhdevflow.devflow-routing-intake
andauthored
Avoid ArrayList copying from TraceInterceptors (#10828)
Skip copying to another array if interceptedTrace == trace 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 Merge branch 'master' into dougqh/trace-intercept-optimization spotless Merge branch 'dougqh/trace-intercept-optimization' of github.com:DataDog/dd-trace-java into dougqh/trace-intercept-optimization Merge branch 'master' into dougqh/trace-intercept-optimization Clean-up - Renamed TraceList -> SpanList - Fix broken test - use SpanList in Groovy test 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. Merge branch 'master' into dougqh/trace-intercept-optimization Adding some Javadoc Merge branch 'master' into dougqh/trace-intercept-optimization codeNarc - removing unnecessary import Merge branch 'master' into dougqh/trace-intercept-optimization Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 1035696 commit 470e304

6 files changed

Lines changed: 204 additions & 24 deletions

File tree

dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,15 +1211,15 @@ public AgentDataStreamsMonitoring getDataStreamsMonitoring() {
12111211
return dataStreamsMonitoring;
12121212
}
12131213

1214-
private final RatelimitedLogger rlLog = new RatelimitedLogger(log, 1, MINUTES);
1214+
private static final RatelimitedLogger rlLog = new RatelimitedLogger(log, 1, MINUTES);
12151215

12161216
/**
12171217
* We use the sampler to know if the trace has to be reported/written. The sampler is called on
12181218
* the first span (root span) of the trace. If the trace is marked as a sample, we report it.
12191219
*
12201220
* @param trace a list of the spans related to the same trace
12211221
*/
1222-
void write(final List<DDSpan> trace) {
1222+
void write(final SpanList trace) {
12231223
if (trace.isEmpty() || !trace.get(0).traceConfig().isTraceEnabled()) {
12241224
return;
12251225
}
@@ -1257,30 +1257,59 @@ void write(final List<DDSpan> trace) {
12571257
}
12581258
}
12591259

1260-
private List<DDSpan> interceptCompleteTrace(List<DDSpan> trace) {
1261-
if (!interceptors.isEmpty() && !trace.isEmpty()) {
1262-
Collection<? extends MutableSpan> interceptedTrace = new ArrayList<>(trace);
1263-
for (final TraceInterceptor interceptor : interceptors) {
1264-
try {
1265-
// If one TraceInterceptor throws an exception, then continue with the next one
1266-
interceptedTrace = interceptor.onTraceComplete(interceptedTrace);
1267-
} catch (Throwable e) {
1268-
String interceptorName = interceptor.getClass().getName();
1269-
rlLog.warn("Throwable raised in TraceInterceptor {}", interceptorName, e);
1270-
}
1271-
if (interceptedTrace == null) {
1272-
interceptedTrace = emptyList();
1273-
}
1260+
private List<DDSpan> interceptCompleteTrace(SpanList originalTrace) {
1261+
return interceptCompleteTrace(interceptors, originalTrace);
1262+
}
1263+
1264+
static final List<DDSpan> interceptCompleteTrace(
1265+
SortedSet<TraceInterceptor> interceptors, SpanList originalTrace) {
1266+
if (interceptors.isEmpty()) {
1267+
return originalTrace;
1268+
}
1269+
if (originalTrace.isEmpty()) {
1270+
return SpanList.EMPTY;
1271+
}
1272+
1273+
// Using TraceList to optimize the common case where the interceptors,
1274+
// don't alter the list. If the interceptors just return the provided
1275+
// List, then no need to copy to another List.
1276+
1277+
// As an extra precaution, also check the modCount before and after on
1278+
// the TraceList, since TraceInterceptor could put some other type of
1279+
// object into the List.
1280+
1281+
// There is still a risk that a TraceInterceptor holds onto the provided
1282+
// List and modifies it later on, but we cannot safeguard against
1283+
// every possible misuse.
1284+
Collection<? extends MutableSpan> interceptedTrace = originalTrace;
1285+
int originalModCount = originalTrace.modCount();
1286+
1287+
for (final TraceInterceptor interceptor : interceptors) {
1288+
try {
1289+
// If one TraceInterceptor throws an exception, then continue with the next one
1290+
interceptedTrace = interceptor.onTraceComplete(interceptedTrace);
1291+
} catch (Throwable e) {
1292+
String interceptorName = interceptor.getClass().getName();
1293+
rlLog.warn("Throwable raised in TraceInterceptor {}", interceptorName, e);
12741294
}
1295+
if (interceptedTrace == null) {
1296+
interceptedTrace = emptyList();
1297+
}
1298+
}
12751299

1276-
trace = new ArrayList<>(interceptedTrace.size());
1300+
if (interceptedTrace == null || interceptedTrace.isEmpty()) {
1301+
return SpanList.EMPTY;
1302+
} else if (interceptedTrace == originalTrace && originalTrace.modCount() == originalModCount) {
1303+
return originalTrace;
1304+
} else {
1305+
SpanList trace = new SpanList(interceptedTrace.size());
12771306
for (final MutableSpan span : interceptedTrace) {
12781307
if (span instanceof DDSpan) {
12791308
trace.add((DDSpan) span);
12801309
}
12811310
}
1311+
return trace;
12821312
}
1283-
return trace;
12841313
}
12851314

12861315
@Override

dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ private int write(boolean isPartial) {
327327
if (!spans.isEmpty()) {
328328
try (Recording recording = tracer.writeTimer()) {
329329
// Only one writer at a time
330-
final List<DDSpan> trace;
330+
final SpanList trace;
331331
int completedSpans = 0;
332332
synchronized (this) {
333333
if (!isPartial) {
@@ -346,10 +346,10 @@ private int write(boolean isPartial) {
346346
// count(s) will be incremented, and any new spans added during the period that the count
347347
// was negative will be written by someone even if we don't write them right now.
348348
if (size > 0 && (!isPartial || size >= tracer.getPartialFlushMinSpans())) {
349-
trace = new ArrayList<>(size);
349+
trace = new SpanList(size);
350350
completedSpans = enqueueSpansToWrite(trace, writeRunningSpans);
351351
} else {
352-
trace = EMPTY;
352+
trace = SpanList.EMPTY;
353353
}
354354
}
355355
if (!trace.isEmpty()) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package datadog.trace.core;
2+
3+
import java.util.ArrayList;
4+
5+
/** ArrayList that exposes modCount to allow for an optimization in TraceInterceptor handling */
6+
final class SpanList extends ArrayList<DDSpan> {
7+
static final SpanList EMPTY = new SpanList(0);
8+
9+
/**
10+
* Convenience function for creating a SpanList containing a single DDSpan. Meant as replacement
11+
* for Collections.singletonList when creating a SpanList.
12+
*
13+
* @param span != null
14+
* @return a SpanList
15+
*/
16+
static final SpanList of(DDSpan span) {
17+
SpanList list = new SpanList(1);
18+
list.add(span);
19+
return list;
20+
}
21+
22+
/**
23+
* Constructs a SpanList with the specified capacity
24+
*
25+
* @param capacity
26+
*/
27+
SpanList(int capacity) {
28+
super(capacity);
29+
}
30+
31+
/** The modifcation count of the List - can be used to check if the List has been altered */
32+
int modCount() {
33+
return this.modCount;
34+
}
35+
}

dd-trace-core/src/main/java/datadog/trace/core/StreamingTraceCollector.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import datadog.trace.api.time.TimeSource;
55
import datadog.trace.bootstrap.instrumentation.api.AgentScope;
66
import datadog.trace.core.monitor.HealthMetrics;
7-
import java.util.Collections;
87
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
98
import javax.annotation.Nonnull;
109

@@ -72,7 +71,7 @@ PublishState onPublish(DDSpan span) {
7271
tracer.onRootSpanPublished(rootSpan);
7372
}
7473
healthMetrics.onFinishSpan();
75-
tracer.write(Collections.singletonList(span));
74+
tracer.write(SpanList.of(span));
7675
return PublishState.WRITTEN;
7776
}
7877

dd-trace-core/src/test/groovy/datadog/trace/core/TraceInterceptorTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ class TraceInterceptorTest extends DDCoreSpecification {
188188
when:
189189
DDSpan span = (DDSpan) tracer.startSpan("test", "test")
190190
span.phasedFinish()
191-
tracer.write([span])
191+
tracer.write(SpanList.of(span))
192192
193193
then:
194194
notThrown(Throwable)

dd-trace-core/src/test/java/datadog/trace/core/CoreTracerTest2.java

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@
88
import static org.junit.jupiter.api.Assertions.assertTrue;
99

1010
import datadog.trace.api.Config;
11+
import datadog.trace.api.interceptor.MutableSpan;
12+
import datadog.trace.api.interceptor.TraceInterceptor;
1113
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
1214
import datadog.trace.core.CoreTracer.CoreSpanBuilder;
1315
import datadog.trace.core.CoreTracer.ReusableSingleSpanBuilder;
1416
import datadog.trace.core.CoreTracer.ReusableSingleSpanBuilderThreadLocalCache;
17+
import java.util.Collection;
18+
import java.util.Collections;
19+
import java.util.List;
20+
import java.util.SortedSet;
21+
import java.util.TreeSet;
1522
import org.junit.jupiter.api.Test;
1623

1724
// named CoreTracerTest2 to avoid collision with Groovy which appears to have messed up test
@@ -179,4 +186,114 @@ public void start_not_inUse() {
179186
ReusableSingleSpanBuilder builder = new ReusableSingleSpanBuilder(TRACER);
180187
assertThrows(AssertionError.class, () -> builder.start());
181188
}
189+
190+
@Test
191+
public void noInterceptorsTest() {
192+
// No interceptors should return the original list to avoid allocation
193+
DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo");
194+
195+
SpanList list = SpanList.of(span);
196+
List<DDSpan> interceptedList =
197+
CoreTracer.interceptCompleteTrace(Collections.emptySortedSet(), list);
198+
assertSame(list, interceptedList);
199+
}
200+
201+
@Test
202+
public void interceptNoInterceptors() {
203+
// No interceptors should return the original list to avoid allocation
204+
DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo");
205+
206+
SpanList list = SpanList.of(span);
207+
List<DDSpan> interceptedList =
208+
CoreTracer.interceptCompleteTrace(Collections.emptySortedSet(), list);
209+
assertSame(list, interceptedList);
210+
}
211+
212+
@Test
213+
public void interceptEmptyList() {
214+
DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo");
215+
SortedSet<TraceInterceptor> interceptors = interceptors((list) -> SpanList.of(span));
216+
217+
SpanList list = new SpanList(0); // not using EMPTY deliberately
218+
List<DDSpan> interceptedList = CoreTracer.interceptCompleteTrace(interceptors, list);
219+
assertTrue(interceptedList.isEmpty());
220+
}
221+
222+
@Test
223+
public void interceptUnchanged() {
224+
SortedSet<TraceInterceptor> interceptors = interceptors((list) -> list, (list) -> list);
225+
226+
DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo");
227+
SpanList list = SpanList.of(span);
228+
List<DDSpan> interceptedList = CoreTracer.interceptCompleteTrace(interceptors, list);
229+
assertSame(list, interceptedList);
230+
}
231+
232+
@Test
233+
public void interceptNewList() {
234+
DDSpan substituteSpan = (DDSpan) TRACER.startSpan("sub", "sub");
235+
SpanList substituteList = SpanList.of(substituteSpan);
236+
SortedSet<TraceInterceptor> interceptors =
237+
interceptors((list) -> list, (list) -> substituteList);
238+
239+
DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo");
240+
SpanList list = SpanList.of(span);
241+
List<DDSpan> interceptedList = CoreTracer.interceptCompleteTrace(interceptors, list);
242+
assertEquals(1, interceptedList.size());
243+
assertEquals("sub", interceptedList.get(0).getOperationName());
244+
}
245+
246+
@Test
247+
public void interceptAlteredList() {
248+
// This is an unlikely case and arguably not something we need to support
249+
250+
DDSpan substituteSpan = (DDSpan) TRACER.startSpan("sub", "sub");
251+
SortedSet<TraceInterceptor> interceptors =
252+
interceptors(
253+
(list) -> list,
254+
(list) -> {
255+
List erasedList = (List) list;
256+
erasedList.clear();
257+
erasedList.add(substituteSpan);
258+
return erasedList;
259+
});
260+
261+
DDSpan span = (DDSpan) TRACER.startSpan("foo", "foo");
262+
SpanList list = SpanList.of(span);
263+
List<DDSpan> interceptedList = CoreTracer.interceptCompleteTrace(interceptors, list);
264+
assertNotSame(interceptedList, list);
265+
assertEquals(1, interceptedList.size());
266+
assertEquals("sub", interceptedList.get(0).getOperationName());
267+
}
268+
269+
static final SortedSet<TraceInterceptor> interceptors(TestInterceptor... interceptors) {
270+
TreeSet<TraceInterceptor> interceptorSet =
271+
new TreeSet<>((lhs, rhs) -> Integer.compare(lhs.priority(), rhs.priority()));
272+
for (int i = 0; i < interceptors.length; ++i) {
273+
int priority = i;
274+
TestInterceptor interceptor = interceptors[i];
275+
276+
interceptorSet.add(
277+
new TraceInterceptor() {
278+
@Override
279+
public int priority() {
280+
return priority;
281+
}
282+
283+
@Override
284+
public Collection<? extends MutableSpan> onTraceComplete(
285+
Collection<? extends MutableSpan> trace) {
286+
return interceptor.onTraceComplete(trace);
287+
}
288+
});
289+
}
290+
return interceptorSet;
291+
}
292+
293+
// Matches TraceInterceptor but priority is implied in interceptors
294+
// Only having onTraceComplete allows this to @FunctionalInterface and a little nicer for a test
295+
@FunctionalInterface
296+
interface TestInterceptor {
297+
Collection<? extends MutableSpan> onTraceComplete(Collection<? extends MutableSpan> trace);
298+
}
182299
}

0 commit comments

Comments
 (0)