Skip to content

Commit 4c89f79

Browse files
dougqhbric3
andauthored
Reuse SpanBuilder within a Thread (#9537)
* Experimenting with introducing CoreSpanBuilder reuse Initial performance experiment The idea is to store a CoreSpanBuilder per thread, since usually only SpanBuilder is in use at a given time per thread -- and CoreSpanBuilder isn't thread safe This simple change provides a giant boost in small heaps Improving Spring petclinic throughput from -39% to -19% with 80m heap * Adding protection against reusing a CoreSpanBuilder that's still in-use * spotless * Adding configuration option to control reuse of SpanBuilders * Adding explanatory comments * Enabling by default * Adding reuse tests Refactored code, so tests work regardless of Config * spotless * Changed the API to be safe for atypical usage To avoid breaking any potential code that builds multiple spans from the same SpanBuilder, updated the SpanBuilder pooling approach Introduced a new method singleSpanBuilder which can build one and only one span, this method can be used by automatic instrumentation as an optimization. singleSpanBuilder is now used inside the startSpan convenience methods, since we know they only build and return one span. Any automatic instrumentation using startSpan gets the optimization for free. buildSpan maintains its original semantics, so all existing continues to work as is. * Fleshing out tests * spotless * Improving single threaded performance In a microbenchmark, buildSpan was performing worse than previously. To address, that shortcoming and to clean-up the code... Made CoreSpanBuilder abstract and introduced two child classes: MultiSpanBuilder and ReusableSingleSpanBuilder MultiSpanBuilder is used by buildSpan ReusableSingleSpanBuilder is used by singleSpanBuilder / startSpan (indirectly) * spotless * Fixing test that renaming didn't update properly * Adding benchmarks for span creation * spotless * Adding clarifying comments & more tests * spotless * tweaking comments * More comments * More comment clean-up * Addressing review feedback Added an init instead of calling reset when creating a new ReusableSingleSpanBuilder Added some asserts to catch misuse of the API * Adding overload to just check major version * Adding ThreadUtils Adding ThreadUtils class that enables checking if Threads are virtual threads * Adding isVirtualThread check to reuseSingleSpanBuilder * Addressing review comments - reduced visibility * Update dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java Co-authored-by: Brice Dutheil <[email protected]> * Update internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java Co-authored-by: Brice Dutheil <[email protected]> * Update dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java Co-authored-by: Brice Dutheil <[email protected]> * Fixing bad update from GitHub suggestion merge * Adding Javadoc * spotless * Update CoreTracer.java Fixing assertion with blackhole test Moved the inUse tracking to start (rather than buildSpan) * Updating comments to reflect the usage logic was moved into start * Tweaking comments * Improving test coverage - exposed bug with not setting inUse in init * spotless * A bit of clean-up - introducing some helper methods * Fixing v-thread tests * CoreTracerTest -> CoreTracerTest2 Seeing if removing naming conflict with Groovy tests, fix Jacoco coverage calculation * Excluded ThreadUtils from coverage - JVM version specific * Addressing review comments - switching to JUnit assume * Adding comment to explain the name --------- Co-authored-by: Brice Dutheil <[email protected]>
1 parent 775e361 commit 4c89f79

File tree

10 files changed

+682
-49
lines changed

10 files changed

+682
-49
lines changed

components/environment/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ val excludedClassesCoverage by extra {
2424
"datadog.environment.JavaVirtualMachine.JvmOptionsHolder", // depends on OS and JVM vendor
2525
"datadog.environment.JvmOptions", // depends on OS and JVM vendor
2626
"datadog.environment.OperatingSystem**", // depends on OS
27+
"datadog.environment.ThreadUtils", // depends on JVM version
2728
)
2829
}
2930
val excludedClassesBranchCoverage by extra {

components/environment/src/main/java/datadog/environment/JavaVersion.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ public boolean is(int major, int minor, int update) {
8787
return this.major == major && this.minor == minor && this.update == update;
8888
}
8989

90+
public boolean isAtLeast(int major) {
91+
return isAtLeast(major, 0, 0);
92+
}
93+
9094
public boolean isAtLeast(int major, int minor, int update) {
9195
return isAtLeast(this.major, this.minor, this.update, major, minor, update);
9296
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package datadog.environment;
2+
3+
import java.lang.invoke.MethodHandle;
4+
import java.lang.invoke.MethodHandles;
5+
import java.lang.invoke.MethodType;
6+
7+
/**
8+
* Helper class for working with Threads
9+
*
10+
* <p>Uses feature detection and provides static helpers to work with different versions of Java
11+
*
12+
* <p>This class is designed to use MethodHandles that constant propagate to minimize the overhead
13+
*/
14+
public final class ThreadUtils {
15+
static final MethodHandle H_IS_VIRTUAL = lookupIsVirtual();
16+
static final MethodHandle H_ID = lookupId();
17+
18+
private ThreadUtils() {}
19+
20+
/** Provides the best id available for the Thread Uses threadId on 19+; getId on older JVMs */
21+
public static final long threadId(Thread thread) {
22+
try {
23+
return (long) H_ID.invoke(thread);
24+
} catch (Throwable t) {
25+
return 0L;
26+
}
27+
}
28+
29+
/** Indicates whether virtual threads are supported on this JVM */
30+
public static final boolean supportsVirtualThreads() {
31+
return (H_IS_VIRTUAL != null);
32+
}
33+
34+
/** Indicates if the current thread is a virtual thread */
35+
public static final boolean isCurrentThreadVirtual() {
36+
// H_IS_VIRTUAL will constant propagate -- then dead code eliminate -- and inline as needed
37+
try {
38+
return (H_IS_VIRTUAL != null) && (boolean) H_IS_VIRTUAL.invoke(Thread.currentThread());
39+
} catch (Throwable t) {
40+
return false;
41+
}
42+
}
43+
44+
/** Indicates if the provided thread is a virtual thread */
45+
public static final boolean isVirtual(Thread thread) {
46+
// H_IS_VIRTUAL will constant propagate -- then dead code eliminate -- and inline as needed
47+
try {
48+
return (H_IS_VIRTUAL != null) && (boolean) H_IS_VIRTUAL.invoke(thread);
49+
} catch (Throwable t) {
50+
return false;
51+
}
52+
}
53+
54+
private static final MethodHandle lookupIsVirtual() {
55+
try {
56+
return MethodHandles.lookup()
57+
.findVirtual(Thread.class, "isVirtual", MethodType.methodType(boolean.class));
58+
} catch (NoSuchMethodException | IllegalAccessException e) {
59+
return null;
60+
}
61+
}
62+
63+
private static final MethodHandle lookupId() {
64+
MethodHandle threadIdHandle = lookupThreadId();
65+
return threadIdHandle != null ? threadIdHandle : lookupGetId();
66+
}
67+
68+
private static final MethodHandle lookupThreadId() {
69+
try {
70+
return MethodHandles.lookup()
71+
.findVirtual(Thread.class, "threadId", MethodType.methodType(long.class));
72+
} catch (NoSuchMethodException | IllegalAccessException e) {
73+
return null;
74+
}
75+
}
76+
77+
private static final MethodHandle lookupGetId() {
78+
try {
79+
return MethodHandles.lookup()
80+
.findVirtual(Thread.class, "getId", MethodType.methodType(long.class));
81+
} catch (NoSuchMethodException | IllegalAccessException e) {
82+
return null;
83+
}
84+
}
85+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package datadog.environment;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
7+
8+
import java.lang.invoke.MethodHandle;
9+
import java.lang.invoke.MethodHandles;
10+
import java.lang.invoke.MethodType;
11+
import java.util.concurrent.ExecutionException;
12+
import java.util.concurrent.ExecutorService;
13+
import java.util.concurrent.Executors;
14+
import java.util.concurrent.atomic.AtomicBoolean;
15+
import org.junit.jupiter.api.Test;
16+
17+
public class ThreadUtilsTest {
18+
@Test
19+
public void threadId() throws InterruptedException {
20+
Thread thread = new Thread("foo");
21+
thread.start();
22+
try {
23+
// always works on Thread's where getId isn't overridden by child class
24+
assertEquals(thread.getId(), ThreadUtils.threadId(thread));
25+
} finally {
26+
thread.join();
27+
}
28+
}
29+
30+
@Test
31+
public void supportsVirtualThreads() {
32+
assertEquals(
33+
JavaVersion.getRuntimeVersion().isAtLeast(21), ThreadUtils.supportsVirtualThreads());
34+
}
35+
36+
@Test
37+
public void isVirtualThread_false() throws InterruptedException {
38+
Thread thread = new Thread("foo");
39+
thread.start();
40+
try {
41+
assertFalse(ThreadUtils.isVirtual(thread));
42+
} finally {
43+
thread.join();
44+
}
45+
}
46+
47+
@Test
48+
public void isCurrentThreadVirtual_false() throws InterruptedException, ExecutionException {
49+
ExecutorService executor = Executors.newSingleThreadExecutor();
50+
try {
51+
assertFalse(executor.submit(() -> ThreadUtils.isCurrentThreadVirtual()).get());
52+
} finally {
53+
executor.shutdown();
54+
}
55+
}
56+
57+
@Test
58+
public void isVirtualThread_true() throws InterruptedException {
59+
assumeTrue(ThreadUtils.supportsVirtualThreads());
60+
61+
Thread vThread = startVirtualThread(() -> {});
62+
try {
63+
assertTrue(ThreadUtils.isVirtual(vThread));
64+
} finally {
65+
vThread.join();
66+
}
67+
}
68+
69+
@Test
70+
public void isCurrentThreadVirtual_true() throws InterruptedException {
71+
assumeTrue(ThreadUtils.supportsVirtualThreads());
72+
73+
AtomicBoolean result = new AtomicBoolean();
74+
75+
Thread vThread =
76+
startVirtualThread(
77+
() -> {
78+
result.set(ThreadUtils.isCurrentThreadVirtual());
79+
});
80+
81+
vThread.join();
82+
assertTrue(result.get());
83+
}
84+
85+
/*
86+
* Should only be called on JVMs that support virtual threads
87+
*/
88+
static final Thread startVirtualThread(Runnable runnable) {
89+
MethodHandle h_startVThread;
90+
try {
91+
h_startVThread =
92+
MethodHandles.lookup()
93+
.findStatic(
94+
Thread.class,
95+
"startVirtualThread",
96+
MethodType.methodType(Thread.class, Runnable.class));
97+
} catch (NoSuchMethodException | IllegalAccessException e) {
98+
throw new IllegalStateException(e);
99+
}
100+
101+
try {
102+
return (Thread) h_startVThread.invoke(runnable);
103+
} catch (Throwable e) {
104+
throw new IllegalStateException(e);
105+
}
106+
}
107+
}

dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public final class GeneralConfig {
110110
public static final String OPTIMIZED_MAP_ENABLED = "optimized.map.enabled";
111111
public static final String TAG_NAME_UTF8_CACHE_SIZE = "tag.name.utf8.cache.size";
112112
public static final String TAG_VALUE_UTF8_CACHE_SIZE = "tag.value.utf8.cache.size";
113+
public static final String SPAN_BUILDER_REUSE_ENABLED = "span.builder.reuse.enabled";
113114
public static final String STACK_TRACE_LENGTH_LIMIT = "stack.trace.length.limit";
114115

115116
public static final String SSI_INJECTION_ENABLED = "injection.enabled";
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package datadog.trace.core;
2+
3+
import static java.util.concurrent.TimeUnit.MICROSECONDS;
4+
5+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
6+
import org.openjdk.jmh.annotations.Benchmark;
7+
import org.openjdk.jmh.annotations.BenchmarkMode;
8+
import org.openjdk.jmh.annotations.Fork;
9+
import org.openjdk.jmh.annotations.Measurement;
10+
import org.openjdk.jmh.annotations.Mode;
11+
import org.openjdk.jmh.annotations.OutputTimeUnit;
12+
import org.openjdk.jmh.annotations.Scope;
13+
import org.openjdk.jmh.annotations.State;
14+
import org.openjdk.jmh.annotations.Threads;
15+
import org.openjdk.jmh.annotations.Warmup;
16+
17+
/**
18+
* Benchmark of key operations of the CoreTracer
19+
*
20+
* <p>NOTE: This is a multi-threaded benchmark; single threaded benchmarks don't accurately reflect
21+
* some of the optimizations.
22+
*
23+
* <p>Use -t 1, if you'd like to do a single threaded run
24+
*/
25+
@State(Scope.Benchmark)
26+
@Warmup(iterations = 3)
27+
@Measurement(iterations = 5)
28+
@BenchmarkMode(Mode.Throughput)
29+
@Threads(8)
30+
@OutputTimeUnit(MICROSECONDS)
31+
@Fork(value = 1)
32+
public class CoreTracerBenchmark {
33+
static final CoreTracer TRACER = CoreTracer.builder().build();
34+
35+
@Benchmark
36+
public AgentSpan startSpan() {
37+
return TRACER.startSpan("foo", "bar");
38+
}
39+
40+
@Benchmark
41+
public AgentSpan buildSpan() {
42+
return TRACER.buildSpan("foo", "bar").start();
43+
}
44+
45+
@Benchmark
46+
public AgentSpan singleSpanBuilder() {
47+
return TRACER.singleSpanBuilder("foo", "bar").start();
48+
}
49+
}

0 commit comments

Comments
 (0)