diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/BoxedContextStore.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/BoxedContextStore.java new file mode 100644 index 00000000000..092652467a1 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/BoxedContextStore.java @@ -0,0 +1,39 @@ +package datadog.trace.bootstrap; + +import datadog.instrument.fieldinject.ObjectStoreDispatch; +import java.util.function.Function; + +/** Boxes a store-id and dispatches context operations through {@link ObjectStoreDispatch}. */ +public final class BoxedContextStore implements ContextStore { + final int storeId; + + BoxedContextStore(final int storeId) { + this.storeId = storeId; + } + + @Override + public Object get(final Object key) { + return ObjectStoreDispatch.get(key, storeId); + } + + @Override + public void put(final Object key, final Object context) { + ObjectStoreDispatch.put(key, storeId, context); + } + + @Override + public Object getOrPut(final Object key, final Object context) { + return ObjectStoreDispatch.getOrPut(key, storeId, context); + } + + @Override + public Object getOrCompute( + final Object key, final Function contextFactory) { + return ObjectStoreDispatch.getOrCompute(key, storeId, contextFactory); + } + + @Override + public Object remove(final Object key) { + return ObjectStoreDispatch.remove(key, storeId); + } +} diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/BoxedContextStores.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/BoxedContextStores.java new file mode 100644 index 00000000000..380386224ed --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/BoxedContextStores.java @@ -0,0 +1,133 @@ +package datadog.trace.bootstrap; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Intentional copy of FieldBackedContextStores but for {@link BoxedContextStore}. + * + *

Only one is loaded at runtime; the explicit field type helps the JIT optimize. + */ +public final class BoxedContextStores { + + private static final Logger log = LoggerFactory.getLogger(BoxedContextStores.class); + + // these fields will be accessed directly from field-injected instrumentation + public static final BoxedContextStore contextStore0 = new BoxedContextStore(0); + public static final BoxedContextStore contextStore1 = new BoxedContextStore(1); + public static final BoxedContextStore contextStore2 = new BoxedContextStore(2); + public static final BoxedContextStore contextStore3 = new BoxedContextStore(3); + public static final BoxedContextStore contextStore4 = new BoxedContextStore(4); + public static final BoxedContextStore contextStore5 = new BoxedContextStore(5); + public static final BoxedContextStore contextStore6 = new BoxedContextStore(6); + public static final BoxedContextStore contextStore7 = new BoxedContextStore(7); + public static final BoxedContextStore contextStore8 = new BoxedContextStore(8); + public static final BoxedContextStore contextStore9 = new BoxedContextStore(9); + public static final BoxedContextStore contextStore10 = new BoxedContextStore(10); + public static final BoxedContextStore contextStore11 = new BoxedContextStore(11); + public static final BoxedContextStore contextStore12 = new BoxedContextStore(12); + public static final BoxedContextStore contextStore13 = new BoxedContextStore(13); + public static final BoxedContextStore contextStore14 = new BoxedContextStore(14); + public static final BoxedContextStore contextStore15 = new BoxedContextStore(15); + public static final BoxedContextStore contextStore16 = new BoxedContextStore(16); + public static final BoxedContextStore contextStore17 = new BoxedContextStore(17); + public static final BoxedContextStore contextStore18 = new BoxedContextStore(18); + public static final BoxedContextStore contextStore19 = new BoxedContextStore(19); + public static final BoxedContextStore contextStore20 = new BoxedContextStore(20); + public static final BoxedContextStore contextStore21 = new BoxedContextStore(21); + public static final BoxedContextStore contextStore22 = new BoxedContextStore(22); + public static final BoxedContextStore contextStore23 = new BoxedContextStore(23); + public static final BoxedContextStore contextStore24 = new BoxedContextStore(24); + public static final BoxedContextStore contextStore25 = new BoxedContextStore(25); + public static final BoxedContextStore contextStore26 = new BoxedContextStore(26); + public static final BoxedContextStore contextStore27 = new BoxedContextStore(27); + public static final BoxedContextStore contextStore28 = new BoxedContextStore(28); + public static final BoxedContextStore contextStore29 = new BoxedContextStore(29); + public static final BoxedContextStore contextStore30 = new BoxedContextStore(30); + public static final BoxedContextStore contextStore31 = new BoxedContextStore(31); + + // keep track of all allocated stores so far + private static volatile BoxedContextStore[] stores = { + contextStore0, + contextStore1, + contextStore2, + contextStore3, + contextStore4, + contextStore5, + contextStore6, + contextStore7, + contextStore8, + contextStore9, + contextStore10, + contextStore11, + contextStore12, + contextStore13, + contextStore14, + contextStore15, + contextStore16, + contextStore17, + contextStore18, + contextStore19, + contextStore20, + contextStore21, + contextStore22, + contextStore23, + contextStore24, + contextStore25, + contextStore26, + contextStore27, + contextStore28, + contextStore29, + contextStore30, + contextStore31 + }; + + private static final ConcurrentHashMap storesByName = + new ConcurrentHashMap<>(); + private static final Object allocationLock = new Object(); + + private static int nextStoreId; + + public static int getContextStoreId(String keyClassName, String contextClassName) { + String storeName = keyClassName + ';' + contextClassName; + BoxedContextStore store = storesByName.get(storeName); + if (store == null) { + synchronized (allocationLock) { + // speculatively create the next store in the sequence and attempt to map this name to it; + // if another thread has mapped this name then the store will be kept for the next mapping + BoxedContextStore existing = + storesByName.putIfAbsent(storeName, store = allocateStore(nextStoreId)); + if (existing != null) { + return existing.storeId; + } + nextStoreId++; + } + log.debug( + "Allocated ContextStore #{} - instrumentation.target.context={}->{}", + store.storeId, + keyClassName, + contextClassName); + } + return store.storeId; + } + + public static BoxedContextStore getContextStore(final int storeId) { + return stores[storeId]; // guaranteed to be populated by getContextStoreId + } + + // this method should only be called while holding the allocation lock + private static BoxedContextStore allocateStore(int storeId) { + BoxedContextStore[] snapshot = stores; + if (storeId >= snapshot.length) { + stores = snapshot = Arrays.copyOf(snapshot, storeId + 16); + } + // check for pre-allocated / speculatively allocated stores + BoxedContextStore store = snapshot[storeId]; + if (store == null) { + snapshot[storeId] = store = new BoxedContextStore(storeId); + } + return store; + } +} diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/ContextStores.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/ContextStores.java new file mode 100644 index 00000000000..ceab1ef3ca1 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/ContextStores.java @@ -0,0 +1,39 @@ +package datadog.trace.bootstrap; + +import datadog.trace.api.InstrumenterConfig; + +/** Allocates {@link ContextStore}s. */ +public final class ContextStores { + + // fast lookup available for fixed number of stores + public static final int FAST_STORE_ID_LIMIT = 32; + + private static final boolean MAP_PER_STORE = + InstrumenterConfig.get().isRuntimeContextMapPerStore(); + + public static final String STORES_DESCRIPTOR = + MAP_PER_STORE + ? "Ldatadog/trace/bootstrap/FieldBackedContextStores;" + : "Ldatadog/trace/bootstrap/BoxedContextStores;"; + + public static final String STORE_DESCRIPTOR = + MAP_PER_STORE + ? "Ldatadog/trace/bootstrap/FieldBackedContextStore;" + : "Ldatadog/trace/bootstrap/BoxedContextStore;"; + + public static int getContextStoreId(String keyClassName, String contextClassName) { + if (MAP_PER_STORE) { + return FieldBackedContextStores.getContextStoreId(keyClassName, contextClassName); + } else { + return BoxedContextStores.getContextStoreId(keyClassName, contextClassName); + } + } + + public static ContextStore getContextStore(final int storeId) { + if (MAP_PER_STORE) { + return FieldBackedContextStores.getContextStore(storeId); + } else { + return BoxedContextStores.getContextStore(storeId); + } + } +} diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/FieldBackedContextStore.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/FieldBackedContextStore.java index 8fcd40eac43..6373bccba7c 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/FieldBackedContextStore.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/FieldBackedContextStore.java @@ -1,8 +1,6 @@ package datadog.trace.bootstrap; import datadog.instrument.fieldinject.KeyWithValue; -import datadog.instrument.fieldinject.ObjectStoreDispatch; -import datadog.trace.api.InstrumenterConfig; import java.util.function.Function; /** @@ -10,9 +8,6 @@ * fields. Delegates to a lazy {@link WeakMap} for keys that don't have a field for this store. */ public final class FieldBackedContextStore implements ContextStore { - private static final boolean MAP_PER_STORE = - InstrumenterConfig.get().isRuntimeContextMapPerStore(); - final int storeId; FieldBackedContextStore(final int storeId) { @@ -23,10 +18,8 @@ public final class FieldBackedContextStore implements ContextStore contextFactory) { + public Object getOrCompute( + final Object key, final Function contextFactory) { if (key instanceof KeyWithValue) { final KeyWithValue accessor = (KeyWithValue) key; Object existingContext = accessor.$get$__dd_instrument$(storeId); @@ -78,15 +68,13 @@ public Object getOrCompute(Object key, Function contextF } } return existingContext; - } else if (MAP_PER_STORE) { - return weakStore().getOrCompute(key, contextFactory); } else { - return ObjectStoreDispatch.getOrCompute(key, storeId, contextFactory); + return weakStore().getOrCompute(key, contextFactory); } } @Override - public Object remove(Object key) { + public Object remove(final Object key) { if (key instanceof KeyWithValue) { final KeyWithValue accessor = (KeyWithValue) key; Object existingContext = accessor.$get$__dd_instrument$(storeId); @@ -99,10 +87,8 @@ public Object remove(Object key) { } } return existingContext; - } else if (MAP_PER_STORE) { - return weakStore().remove(key); } else { - return ObjectStoreDispatch.remove(key, storeId); + return weakStore().remove(key); } } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/FieldBackedContextStores.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/FieldBackedContextStores.java index 9b064829930..d59f34be2cb 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/FieldBackedContextStores.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/FieldBackedContextStores.java @@ -10,9 +10,6 @@ public final class FieldBackedContextStores { private static final Logger log = LoggerFactory.getLogger(FieldBackedContextStores.class); - // provide fast lookup for a fixed number of stores - public static final int FAST_STORE_ID_LIMIT = 32; - // these fields will be accessed directly from field-injected instrumentation public static final FieldBackedContextStore contextStore0 = new FieldBackedContextStore(0); public static final FieldBackedContextStore contextStore1 = new FieldBackedContextStore(1); diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/FieldBackedContextStoresTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/FieldBackedContextStoresTest.groovy deleted file mode 100644 index 7139df2597e..00000000000 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/FieldBackedContextStoresTest.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package datadog.trace.bootstrap - -import datadog.trace.test.util.ThreadUtils -import datadog.trace.test.util.DDSpecification - -import java.util.concurrent.atomic.AtomicInteger - -class FieldBackedContextStoresTest extends DDSpecification { - - def "test FieldBackedContextStore id allocation"() { - setup: - int testAllocations = 128 - FieldBackedContextStore[] allocatedStores = new FieldBackedContextStore[testAllocations] - AtomicInteger keyIds = new AtomicInteger() - - ThreadUtils.runConcurrently(10, testAllocations, { - int keyId = keyIds.getAndIncrement() - int storeId = FieldBackedContextStores.getContextStoreId("key${keyId}", "value${keyId}") - assert allocatedStores[storeId] == null - allocatedStores[storeId] = FieldBackedContextStores.getContextStore(storeId) - }) - - expect: - keyIds.get() == testAllocations - allocatedStores.size() == testAllocations - (allocatedStores as List).withIndex().collect({ store, storeId -> assert store.storeId == storeId }) - } -} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/BoxedContextStoresForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/BoxedContextStoresForkedTest.java new file mode 100644 index 00000000000..3b5918647c5 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/BoxedContextStoresForkedTest.java @@ -0,0 +1,117 @@ +package datadog.trace.bootstrap; + +import static datadog.trace.test.util.ThreadUtils.runConcurrently; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +/** Forked to isolate the static context-store registry from other tests. */ +class BoxedContextStoresForkedTest { + + private static final int THREAD_COUNT = 8; + + // enough stores to go past the pre-allocated stores, grow the array, and keep threads contending + private static final int STORE_COUNT = ContextStores.FAST_STORE_ID_LIMIT + 2000; + + @Test + void sameNameReturnsSameStore() { + int storeId = BoxedContextStores.getContextStoreId("sameKey", "sameContext"); + assertEquals(storeId, BoxedContextStores.getContextStoreId("sameKey", "sameContext")); + assertNotEquals(storeId, BoxedContextStores.getContextStoreId("sameKey", "otherContext")); + assertNotEquals(storeId, BoxedContextStores.getContextStoreId("otherKey", "sameContext")); + assertEquals(storeId, BoxedContextStores.getContextStore(storeId).storeId); + } + + @Test + void concurrentAllocationAssignsDistinctIdsForDistinctNames() throws Throwable { + int testAllocations = 128; + int firstStoreId = + BoxedContextStores.getContextStoreId("allocationStartKey", "allocationStartContext") + 1; + BoxedContextStore[] allocatedStores = new BoxedContextStore[testAllocations]; + AtomicInteger keyIds = new AtomicInteger(); + + runConcurrently( + 10, + testAllocations, + () -> { + int keyId = keyIds.getAndIncrement(); + int storeId = BoxedContextStores.getContextStoreId("key" + keyId, "value" + keyId); + int index = storeId - firstStoreId; + assertNull(allocatedStores[index]); + allocatedStores[index] = BoxedContextStores.getContextStore(storeId); + }); + + assertEquals(testAllocations, keyIds.get()); + assertEquals(testAllocations, allocatedStores.length); + for (int index = 0; index < testAllocations; index++) { + assertNotNull(allocatedStores[index]); + assertEquals(firstStoreId + index, allocatedStores[index].storeId); + } + } + + @Test + void concurrentAllocationAssignsOneContiguousIdPerName() throws Exception { + List keys = new ArrayList<>(); + for (int i = 0; i < STORE_COUNT; i++) { + keys.add("concurrentKey" + i); + } + + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT); + try { + List> results = new ArrayList<>(); + for (int t = 0; t < THREAD_COUNT; t++) { + // every thread requests the same names in the same order to maximise contention + results.add( + executor.submit( + () -> { + start.await(); + int[] storeIds = new int[STORE_COUNT]; + for (int i = 0; i < STORE_COUNT; i++) { + int storeId = BoxedContextStores.getContextStoreId(keys.get(i), "context"); + // store must be visible as soon as its id is returned + BoxedContextStore store = BoxedContextStores.getContextStore(storeId); + assertNotNull(store, "Missing store for id " + storeId); + assertEquals(storeId, store.storeId); + storeIds[i] = storeId; + } + return storeIds; + })); + } + start.countDown(); + + int[] expectedIds = results.get(0).get(10, SECONDS); + for (Future result : results) { + int[] storeIds = result.get(10, SECONDS); + for (int i = 0; i < STORE_COUNT; i++) { + assertEquals(expectedIds[i], storeIds[i], "Threads disagree on id for " + keys.get(i)); + } + } + + TreeSet distinctIds = new TreeSet<>(); + for (int storeId : expectedIds) { + assertTrue(distinctIds.add(storeId), "Store id " + storeId + " allocated twice"); + BoxedContextStore store = BoxedContextStores.getContextStore(storeId); + assertNotNull(store, "Missing store for id " + storeId); + assertEquals(storeId, store.storeId); + } + assertEquals( + STORE_COUNT - 1, distinctIds.last() - distinctIds.first(), "Store ids have gaps"); + } finally { + executor.shutdownNow(); + } + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/FieldBackedContextStoresForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/FieldBackedContextStoresForkedTest.java index 8a2cef3aac7..bed99c3811e 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/FieldBackedContextStoresForkedTest.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/FieldBackedContextStoresForkedTest.java @@ -1,9 +1,11 @@ package datadog.trace.bootstrap; +import static datadog.trace.test.util.ThreadUtils.runConcurrently; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; @@ -13,6 +15,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; /** Forked to isolate the static context-store registry from other tests. */ @@ -21,7 +24,7 @@ class FieldBackedContextStoresForkedTest { private static final int THREAD_COUNT = 8; // enough stores to go past the pre-allocated stores, grow the array, and keep threads contending - private static final int STORE_COUNT = FieldBackedContextStores.FAST_STORE_ID_LIMIT + 2000; + private static final int STORE_COUNT = ContextStores.FAST_STORE_ID_LIMIT + 2000; @Test void sameNameReturnsSameStore() { @@ -32,6 +35,34 @@ void sameNameReturnsSameStore() { assertEquals(storeId, FieldBackedContextStores.getContextStore(storeId).storeId); } + @Test + void concurrentAllocationAssignsDistinctIdsForDistinctNames() throws Throwable { + int testAllocations = 128; + int firstStoreId = + FieldBackedContextStores.getContextStoreId("allocationStartKey", "allocationStartContext") + + 1; + FieldBackedContextStore[] allocatedStores = new FieldBackedContextStore[testAllocations]; + AtomicInteger keyIds = new AtomicInteger(); + + runConcurrently( + 10, + testAllocations, + () -> { + int keyId = keyIds.getAndIncrement(); + int storeId = FieldBackedContextStores.getContextStoreId("key" + keyId, "value" + keyId); + int index = storeId - firstStoreId; + assertNull(allocatedStores[index]); + allocatedStores[index] = FieldBackedContextStores.getContextStore(storeId); + }); + + assertEquals(testAllocations, keyIds.get()); + assertEquals(testAllocations, allocatedStores.length); + for (int index = 0; index < testAllocations; index++) { + assertNotNull(allocatedStores[index]); + assertEquals(firstStoreId + index, allocatedStores[index].storeId); + } + } + @Test void concurrentAllocationAssignsOneContiguousIdPerName() throws Exception { List keys = new ArrayList<>(); diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java index 1dae022b06c..02fcd63a9de 100644 --- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java +++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java @@ -58,8 +58,10 @@ public class AgentInstaller { static { enableByteBuddyRawTypes(); disableByteBuddyNexus(); - // register weak map supplier as early as possible - WeakMaps.registerAsSupplier(); + if (InstrumenterConfig.get().isRuntimeContextMapPerStore()) { + // register weak map supplier as early as possible + WeakMaps.registerAsSupplier(); + } circularityErrorWorkaround(); } diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java index 6c2dd3e8a6d..e6d92999d05 100644 --- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java +++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java @@ -243,7 +243,7 @@ private void buildTypeAdvice(Instrumenter member, int transformationId) { } if (null != contextRequestRewriter) { registerContextStoreInjection(member, contextStore); - // rewrite context store access to call FieldBackedContextStores with assigned store-id + // rewrite context store access to use assigned store-id advice.add(contextRequestRewriter); } diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelMethodCallMapper.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelMethodCallMapper.java index 3d8021acf84..cf3127073bb 100644 --- a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelMethodCallMapper.java +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelMethodCallMapper.java @@ -1,10 +1,9 @@ package datadog.opentelemetry.tooling; -import static datadog.trace.bootstrap.FieldBackedContextStores.FAST_STORE_ID_LIMIT; -import static datadog.trace.bootstrap.FieldBackedContextStores.getContextStoreId; +import static datadog.trace.bootstrap.ContextStores.FAST_STORE_ID_LIMIT; +import static datadog.trace.bootstrap.ContextStores.getContextStoreId; -import datadog.trace.bootstrap.FieldBackedContextStore; -import datadog.trace.bootstrap.FieldBackedContextStores; +import datadog.trace.bootstrap.ContextStores; import net.bytebuddy.jar.asm.MethodVisitor; import net.bytebuddy.jar.asm.Opcodes; import net.bytebuddy.jar.asm.Type; @@ -17,15 +16,12 @@ public final class OtelMethodCallMapper extends MethodRemapper { private static final String VIRTUAL_FIELD_CLASS = "io/opentelemetry/javaagent/shaded/instrumentation/api/util/VirtualField"; - private static final String FIELD_BACKED_CONTEXT_STORES_CLASS = - Type.getInternalName(FieldBackedContextStores.class); + private static final String CONTEXT_STORES_FACTORY_CLASS = + Type.getType(ContextStores.STORES_DESCRIPTOR).getInternalName(); private static final String GET_CONTENT_STORE_METHOD = "getContextStore"; private static final String GET_CONTENT_STORE_METHOD_DESCRIPTOR = - Type.getMethodDescriptor(Type.getType(FieldBackedContextStore.class), Type.INT_TYPE); - - private static final String FIELD_BACKED_CONTENT_STORE_DESCRIPTOR = - Type.getDescriptor(FieldBackedContextStore.class); + Type.getMethodDescriptor(Type.getType(ContextStores.STORE_DESCRIPTOR), Type.INT_TYPE); private static final String FAST_CONTENT_STORE_PREFIX = "contextStore"; @@ -98,14 +94,14 @@ private void redirectVirtualFieldLookup() { if (storeId < FAST_STORE_ID_LIMIT) { mv.visitFieldInsn( Opcodes.GETSTATIC, - FIELD_BACKED_CONTEXT_STORES_CLASS, + CONTEXT_STORES_FACTORY_CLASS, FAST_CONTENT_STORE_PREFIX + storeId, - FIELD_BACKED_CONTENT_STORE_DESCRIPTOR); + ContextStores.STORE_DESCRIPTOR); } else { mv.visitLdcInsn(storeId); mv.visitMethodInsn( Opcodes.INVOKESTATIC, - FIELD_BACKED_CONTEXT_STORES_CLASS, + CONTEXT_STORES_FACTORY_CLASS, GET_CONTENT_STORE_METHOD, GET_CONTENT_STORE_METHOD_DESCRIPTOR, false); diff --git a/dd-java-agent/agent-tooling/build.gradle b/dd-java-agent/agent-tooling/build.gradle index 7c7d6d20ffc..b13b55cfd73 100644 --- a/dd-java-agent/agent-tooling/build.gradle +++ b/dd-java-agent/agent-tooling/build.gradle @@ -77,6 +77,17 @@ tasks.named("jmh") { outputs.upToDateWhen { false } } +tasks.named("jmhJar", Jar) { + // includeTests also brings in the test InstrumenterModule services file; drop it so only + // the jmh benchmark instrumentations are registered, regardless of copy order + def testResourcesDir = sourceSets.test.output.resourcesDir.toPath() + filesMatching("META-INF/services/datadog.trace.agent.tooling.InstrumenterModule") { + if (it.file.toPath().startsWith(testResourcesDir)) { + it.exclude() + } + } +} + tasks.withType(Test).configureEach { // same setting as AgentInstaller to avoid spurious agent-tooling test failures // caused by ConfigTransformSpockExtension installing byte-buddy during testing diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/ContextStoreBenchmarkKey.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/ContextStoreBenchmarkKey.java new file mode 100644 index 00000000000..0b36df829c9 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/ContextStoreBenchmarkKey.java @@ -0,0 +1,26 @@ +package context.benchmark; + +import java.util.function.Function; + +/** + * Key type for the context-store benchmark; method bodies are replaced by advice. + * + *

Key types live outside {@code datadog.*} because the agent never instruments those packages. + */ +public interface ContextStoreBenchmarkKey { + int read(); + + void write(Object context); + + Object getContext(int storeIndex); + + void putContext(int storeIndex, Object context); + + Object removeContext(int storeIndex); + + Object getOrPutContext(int storeIndex, Object context); + + Object getOrComputeContext(int storeIndex, Function contextFactory); + + ContextStoreBenchmarkKey newKey(); +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/FieldInjectedKey.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/FieldInjectedKey.java new file mode 100644 index 00000000000..c568f6bee59 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/FieldInjectedKey.java @@ -0,0 +1,41 @@ +package context.benchmark; + +import java.util.function.Function; + +public final class FieldInjectedKey implements Store1Key, Store2Key, Store3Key { + @Override + public int read() { + return -1; + } + + @Override + public void write(Object context) {} + + @Override + public Object getContext(int storeIndex) { + return null; + } + + @Override + public void putContext(int storeIndex, Object context) {} + + @Override + public Object removeContext(int storeIndex) { + return null; + } + + @Override + public Object getOrPutContext(int storeIndex, Object context) { + return null; + } + + @Override + public Object getOrComputeContext(int storeIndex, Function contextFactory) { + return null; + } + + @Override + public ContextStoreBenchmarkKey newKey() { + return new FieldInjectedKey(); + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/NonFieldInjectedKey.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/NonFieldInjectedKey.java new file mode 100644 index 00000000000..37199eb5d86 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/NonFieldInjectedKey.java @@ -0,0 +1,41 @@ +package context.benchmark; + +import java.util.function.Function; + +public final class NonFieldInjectedKey implements Store1Key, Store2Key, Store3Key { + @Override + public int read() { + return -1; + } + + @Override + public void write(Object context) {} + + @Override + public Object getContext(int storeIndex) { + return null; + } + + @Override + public void putContext(int storeIndex, Object context) {} + + @Override + public Object removeContext(int storeIndex) { + return null; + } + + @Override + public Object getOrPutContext(int storeIndex, Object context) { + return null; + } + + @Override + public Object getOrComputeContext(int storeIndex, Function contextFactory) { + return null; + } + + @Override + public ContextStoreBenchmarkKey newKey() { + return new NonFieldInjectedKey(); + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/Store1Key.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/Store1Key.java new file mode 100644 index 00000000000..687aaeab6ab --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/Store1Key.java @@ -0,0 +1,3 @@ +package context.benchmark; + +public interface Store1Key extends ContextStoreBenchmarkKey {} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/Store2Key.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/Store2Key.java new file mode 100644 index 00000000000..bf94db512dd --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/Store2Key.java @@ -0,0 +1,3 @@ +package context.benchmark; + +public interface Store2Key extends ContextStoreBenchmarkKey {} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/Store3Key.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/Store3Key.java new file mode 100644 index 00000000000..5980a752622 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/Store3Key.java @@ -0,0 +1,3 @@ +package context.benchmark; + +public interface Store3Key extends ContextStoreBenchmarkKey {} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/InjectedKey.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/InjectedKey.java new file mode 100644 index 00000000000..29f52248225 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/InjectedKey.java @@ -0,0 +1,32 @@ +package context.benchmark.workload; + +import context.benchmark.workload.StoreKeys.S0; +import context.benchmark.workload.StoreKeys.S1; +import context.benchmark.workload.StoreKeys.S10; +import context.benchmark.workload.StoreKeys.S11; +import context.benchmark.workload.StoreKeys.S12; +import context.benchmark.workload.StoreKeys.S13; +import context.benchmark.workload.StoreKeys.S14; +import context.benchmark.workload.StoreKeys.S15; +import context.benchmark.workload.StoreKeys.S2; +import context.benchmark.workload.StoreKeys.S3; +import context.benchmark.workload.StoreKeys.S4; +import context.benchmark.workload.StoreKeys.S5; +import context.benchmark.workload.StoreKeys.S6; +import context.benchmark.workload.StoreKeys.S7; +import context.benchmark.workload.StoreKeys.S8; +import context.benchmark.workload.StoreKeys.S9; + +/** Key loaded after the agent is installed, so it has injected fields for every store. */ +public final class InjectedKey + implements WorkloadKey, S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, S10, S11, S12, S13, S14, S15 { + @Override + public Object store(int storeIndex) { + return null; + } + + @Override + public WorkloadKey newKey() { + return new InjectedKey(); + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/PartialKey.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/PartialKey.java new file mode 100644 index 00000000000..45de75cf0d4 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/PartialKey.java @@ -0,0 +1,25 @@ +package context.benchmark.workload; + +import context.benchmark.workload.StoreKeys.S0; +import context.benchmark.workload.StoreKeys.S1; +import context.benchmark.workload.StoreKeys.S2; +import context.benchmark.workload.StoreKeys.S3; +import context.benchmark.workload.StoreKeys.S4; +import context.benchmark.workload.StoreKeys.S5; +import context.benchmark.workload.StoreKeys.S6; +import context.benchmark.workload.StoreKeys.S7; + +/** + * Field-injected key that only has fields for stores 0-7, so stores 8-15 redirect to the weak map. + */ +public final class PartialKey extends PartialKeyBase implements S0, S1, S2, S3, S4, S5, S6, S7 { + @Override + public Object store(int storeIndex) { + return null; + } + + @Override + public WorkloadKey newKey() { + return new PartialKey(); + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/PartialKeyBase.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/PartialKeyBase.java new file mode 100644 index 00000000000..940e08e4eea --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/PartialKeyBase.java @@ -0,0 +1,13 @@ +package context.benchmark.workload; + +import context.benchmark.workload.StoreKeys.S10; +import context.benchmark.workload.StoreKeys.S11; +import context.benchmark.workload.StoreKeys.S12; +import context.benchmark.workload.StoreKeys.S13; +import context.benchmark.workload.StoreKeys.S14; +import context.benchmark.workload.StoreKeys.S15; +import context.benchmark.workload.StoreKeys.S8; +import context.benchmark.workload.StoreKeys.S9; + +/** Loaded before the agent so subclasses fall back to the weak map for stores 8-15. */ +public abstract class PartialKeyBase implements WorkloadKey, S8, S9, S10, S11, S12, S13, S14, S15 {} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/StoreKeys.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/StoreKeys.java new file mode 100644 index 00000000000..39e378de85d --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/StoreKeys.java @@ -0,0 +1,38 @@ +package context.benchmark.workload; + +/** One marker interface per context store, so each store has its own key type. */ +public final class StoreKeys { + private StoreKeys() {} + + public interface S0 {} + + public interface S1 {} + + public interface S2 {} + + public interface S3 {} + + public interface S4 {} + + public interface S5 {} + + public interface S6 {} + + public interface S7 {} + + public interface S8 {} + + public interface S9 {} + + public interface S10 {} + + public interface S11 {} + + public interface S12 {} + + public interface S13 {} + + public interface S14 {} + + public interface S15 {} +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/UninjectedKey.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/UninjectedKey.java new file mode 100644 index 00000000000..fc0c3e16216 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/UninjectedKey.java @@ -0,0 +1,32 @@ +package context.benchmark.workload; + +import context.benchmark.workload.StoreKeys.S0; +import context.benchmark.workload.StoreKeys.S1; +import context.benchmark.workload.StoreKeys.S10; +import context.benchmark.workload.StoreKeys.S11; +import context.benchmark.workload.StoreKeys.S12; +import context.benchmark.workload.StoreKeys.S13; +import context.benchmark.workload.StoreKeys.S14; +import context.benchmark.workload.StoreKeys.S15; +import context.benchmark.workload.StoreKeys.S2; +import context.benchmark.workload.StoreKeys.S3; +import context.benchmark.workload.StoreKeys.S4; +import context.benchmark.workload.StoreKeys.S5; +import context.benchmark.workload.StoreKeys.S6; +import context.benchmark.workload.StoreKeys.S7; +import context.benchmark.workload.StoreKeys.S8; +import context.benchmark.workload.StoreKeys.S9; + +/** Key loaded before the agent is installed, so every store falls back to the weak map. */ +public final class UninjectedKey + implements WorkloadKey, S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, S10, S11, S12, S13, S14, S15 { + @Override + public Object store(int storeIndex) { + return null; + } + + @Override + public WorkloadKey newKey() { + return new UninjectedKey(); + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/WorkloadKey.java b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/WorkloadKey.java new file mode 100644 index 00000000000..1afa69b6d0b --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/context/benchmark/workload/WorkloadKey.java @@ -0,0 +1,14 @@ +package context.benchmark.workload; + +/** + * Key type for the context-store workload benchmark. + * + *

Key types live outside {@code datadog.*} because the agent never instruments those packages. + */ +public interface WorkloadKey { + /** Returns the context store with the given index; the method body is replaced by advice. */ + Object store(int storeIndex); + + /** Creates a new key of the same type. */ + WorkloadKey newKey(); +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreBenchmark.java b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreBenchmark.java new file mode 100644 index 00000000000..2971c23135f --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreBenchmark.java @@ -0,0 +1,313 @@ +package datadog.trace.agent.tooling.context.benchmark; + +import static datadog.trace.agent.tooling.context.benchmark.ContextStoreBenchmarkSupport.installAgent; +import static datadog.trace.agent.tooling.context.benchmark.ContextStoreBenchmarkSupport.newKey; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import context.benchmark.ContextStoreBenchmarkKey; +import java.util.function.Function; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.ThreadParams; + +/** + * Compares context-store access for keys with injected fields against keys that fall back to the + * store's map, with {@code map-per-store} both enabled and disabled. + * + *

The advice uses the instrumented {@code this} as the context key, so once the store access is + * inlined C2 knows the exact key type and binds the injected accessor statically. Results therefore + * don't depend on how many other key types have been seen by the shared store code. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(NANOSECONDS) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(2) +public class ContextStoreBenchmark { + @Benchmark + public int fieldInjectedRead(Keys keys) { + return keys.fieldInjected[keys.next()].read(); + } + + @Benchmark + public int nonFieldInjectedRead(Keys keys) { + return keys.nonFieldInjected[keys.next()].read(); + } + + @Benchmark + @OperationsPerInvocation(2) + public int mixedRead(Keys keys) { + int i = keys.next(); + return keys.fieldInjected[i].read() + keys.nonFieldInjected[i].read(); + } + + @Benchmark + public void fieldInjectedWrite(Keys keys) { + keys.fieldInjected[keys.next()].write(keys.context); + } + + @Benchmark + public void nonFieldInjectedWrite(Keys keys) { + keys.nonFieldInjected[keys.next()].write(keys.context); + } + + @Benchmark + @OperationsPerInvocation(2) + public void mixedWrite(Keys keys) { + int i = keys.next(); + keys.fieldInjected[i].write(keys.context); + keys.nonFieldInjected[i].write(keys.context); + } + + /** Reads an existing context without replacing it with the supplied value. */ + @Benchmark + public Object fieldInjectedGetOrPutHit(Keys keys) { + return keys.fieldInjected[keys.next()].getOrPutContext(0, keys.alternateContext); + } + + @Benchmark + public Object nonFieldInjectedGetOrPutHit(Keys keys) { + return keys.nonFieldInjected[keys.next()].getOrPutContext(0, keys.alternateContext); + } + + /** Reads an existing context without invoking the factory. */ + @Benchmark + public Object fieldInjectedGetOrComputeHit(Keys keys) { + return keys.fieldInjected[keys.next()].getOrComputeContext(0, keys.contextFactory); + } + + @Benchmark + public Object nonFieldInjectedGetOrComputeHit(Keys keys) { + return keys.nonFieldInjected[keys.next()].getOrComputeContext(0, keys.contextFactory); + } + + /** Models request-local context association without making application keys shared by threads. */ + @Benchmark + public boolean mixedRequestLifecycle(LifecycleKeys keys) { + int i = keys.next(); + int storeIndex = keys.storeIndex(i); + ContextStoreBenchmarkKey fieldInjected = keys.fieldInjected[i]; + ContextStoreBenchmarkKey nonFieldInjected = keys.nonFieldInjected[i]; + Object context = keys.contexts[storeIndex]; + + fieldInjected.putContext(storeIndex, context); + nonFieldInjected.putContext(storeIndex, context); + + boolean observed = true; + for (int j = 0; j < 4; j++) { + observed &= fieldInjected.getContext(storeIndex) == context; + observed &= nonFieldInjected.getContext(storeIndex) == context; + } + + observed &= fieldInjected.removeContext(storeIndex) == context; + observed &= nonFieldInjected.removeContext(storeIndex) == context; + return observed; + } + + /** + * A companion to the churn workload with literal store IDs in instrumented key methods. Each + * invocation allocates fresh keys, assigns two contexts, reads them, and removes them. This + * isolates request-local access; it does not model abandoned keys or delayed retirement. Use + * {@code -p freshContexts=true} to allocate contexts as well as keys. + */ + @Benchmark + public boolean constantStoreRequestLifecycle(FreshKeys keys) { + ContextStoreBenchmarkKey injected = keys.fieldInjected.newKey(); + ContextStoreBenchmarkKey fallback = keys.nonFieldInjected.newKey(); + Object context = + keys.freshContexts ? new ContextStoreBenchmarkInstrumentation.State(42) : keys.context; + Object secondContext = + keys.freshContexts + ? new ContextStoreBenchmarkInstrumentation.State1(42) + : keys.secondContext; + injected.putContext(0, context); + fallback.putContext(0, context); + injected.putContext(1, secondContext); + fallback.putContext(1, secondContext); + boolean observed = true; + for (int i = 0; i < 4; i++) { + observed &= injected.getContext(0) == context; + observed &= fallback.getContext(0) == context; + observed &= injected.getContext(1) == secondContext; + observed &= fallback.getContext(1) == secondContext; + } + observed &= injected.removeContext(0) == context; + observed &= fallback.removeContext(0) == context; + observed &= injected.removeContext(1) == secondContext; + observed &= fallback.removeContext(1) == secondContext; + return observed; + } + + @State(Scope.Thread) + public static class FreshKeys { + @Param("false") + public boolean freshContexts; + + private ContextStoreBenchmarkKey fieldInjected; + private ContextStoreBenchmarkKey nonFieldInjected; + private final Object context = new ContextStoreBenchmarkInstrumentation.State(42); + private final Object secondContext = new ContextStoreBenchmarkInstrumentation.State1(42); + + @Setup(Level.Trial) + public void setUp(InstrumentedTypes types) throws Exception { + fieldInjected = types.newFieldInjectedKeys(1)[0]; + nonFieldInjected = types.newNonFieldInjectedKeys(1)[0]; + if (!new ContextStoreBenchmark().constantStoreRequestLifecycle(this)) { + throw new IllegalStateException("constant-store lifecycle instrumentation is not active"); + } + } + } + + @State(Scope.Benchmark) + public static class InstrumentedTypes { + @Param({"true", "false"}) + public boolean mapPerStore; + + @Setup(Level.Trial) + public void setUp() throws Exception { + installAgent(mapPerStore); + } + + ContextStoreBenchmarkKey[] newFieldInjectedKeys(int count) throws Exception { + return newKeys("context.benchmark.FieldInjectedKey", count, true); + } + + ContextStoreBenchmarkKey[] newNonFieldInjectedKeys(int count) throws Exception { + return newKeys("context.benchmark.NonFieldInjectedKey", count, false); + } + } + + @State(Scope.Thread) + public static class Keys { + @Param({"1", "1024"}) + public int keyCount; + + private ContextStoreBenchmarkKey[] fieldInjected; + private ContextStoreBenchmarkKey[] nonFieldInjected; + private final Object context = new ContextStoreBenchmarkInstrumentation.State(42); + private final Object alternateContext = new ContextStoreBenchmarkInstrumentation.State(-1); + private final Function contextFactory = key -> alternateContext; + private int index; + + @Setup(Level.Trial) + public void setUp(InstrumentedTypes types) throws Exception { + assertPowerOfTwo("keyCount", keyCount); + fieldInjected = types.newFieldInjectedKeys(keyCount); + nonFieldInjected = types.newNonFieldInjectedKeys(keyCount); + + Function unexpectedFactory = + key -> { + throw new IllegalStateException("getOrCompute invoked the factory for a populated key"); + }; + for (int i = 0; i < keyCount; i++) { + fieldInjected[i].write(context); + nonFieldInjected[i].write(context); + if (fieldInjected[i].read() != 42 + || nonFieldInjected[i].read() != 42 + || fieldInjected[i].getOrPutContext(0, alternateContext) != context + || nonFieldInjected[i].getOrPutContext(0, alternateContext) != context + || fieldInjected[i].getOrComputeContext(0, unexpectedFactory) != context + || nonFieldInjected[i].getOrComputeContext(0, unexpectedFactory) != context) { + throw new IllegalStateException("context-store instrumentation is not active"); + } + } + } + + /** Returns the current key index and advances to the next key. */ + int next() { + int i = index; + index = (i + 1) & (keyCount - 1); + return i; + } + } + + @State(Scope.Thread) + public static class LifecycleKeys { + @Param("1024") + public int keyCount; + + @Param({"1", "4"}) + public int storeCount; + + private ContextStoreBenchmarkKey[] fieldInjected; + private ContextStoreBenchmarkKey[] nonFieldInjected; + private Object[] contexts; + private int index; + private int threadIndex; + + @Setup(Level.Trial) + public void setUp(InstrumentedTypes types, ThreadParams threadParams) throws Exception { + assertPowerOfTwo("keyCount", keyCount); + assertPowerOfTwo("storeCount", storeCount); + if (storeCount > 4) { + throw new IllegalArgumentException("storeCount must be 1, 2, or 4"); + } + fieldInjected = types.newFieldInjectedKeys(keyCount); + nonFieldInjected = types.newNonFieldInjectedKeys(keyCount); + threadIndex = threadParams.getThreadIndex(); + contexts = + new Object[] { + new ContextStoreBenchmarkInstrumentation.State(42), + new ContextStoreBenchmarkInstrumentation.State1(42), + new ContextStoreBenchmarkInstrumentation.State2(42), + new ContextStoreBenchmarkInstrumentation.State3(42) + }; + verifyStores(); + } + + private void verifyStores() { + for (int storeIndex = 0; storeIndex < storeCount; storeIndex++) { + Object context = contexts[storeIndex]; + fieldInjected[0].putContext(storeIndex, context); + nonFieldInjected[0].putContext(storeIndex, context); + if (fieldInjected[0].getContext(storeIndex) != context + || nonFieldInjected[0].getContext(storeIndex) != context + || fieldInjected[0].removeContext(storeIndex) != context + || nonFieldInjected[0].removeContext(storeIndex) != context) { + throw new IllegalStateException("context store " + storeIndex + " is not active"); + } + } + } + + /** Returns the current key index and advances to the next key. */ + int next() { + int i = index; + index = (i + 1) & (keyCount - 1); + return i; + } + + /** + * Rotates through the stores, offset by thread so concurrent threads don't all hit the same + * store in lock-step. + */ + int storeIndex(int keyIndex) { + return (keyIndex + threadIndex) & (storeCount - 1); + } + } + + private static ContextStoreBenchmarkKey[] newKeys(String typeName, int count, boolean injected) + throws Exception { + ContextStoreBenchmarkKey[] keys = new ContextStoreBenchmarkKey[count]; + for (int i = 0; i < count; i++) { + keys[i] = newKey(typeName, injected); + } + return keys; + } + + private static void assertPowerOfTwo(String name, int value) { + if (Integer.bitCount(value) != 1) { + throw new IllegalArgumentException(name + " must be a power of two"); + } + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreBenchmarkInstrumentation.java b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreBenchmarkInstrumentation.java new file mode 100644 index 00000000000..68630947681 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreBenchmarkInstrumentation.java @@ -0,0 +1,252 @@ +package datadog.trace.agent.tooling.context.benchmark; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; + +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.agent.tooling.muzzle.ReferenceMatcher; +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.InstrumentationContext; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public final class ContextStoreBenchmarkInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { + private static final String KEY_TYPE = "context.benchmark.ContextStoreBenchmarkKey"; + private static final String CONTEXT_TYPE = + "datadog.trace.agent.tooling.context.benchmark.ContextStoreBenchmarkInstrumentation$State"; + private static final String STORE1_KEY_TYPE = "context.benchmark.Store1Key"; + private static final String STORE1_CONTEXT_TYPE = CONTEXT_TYPE + "1"; + private static final String STORE2_KEY_TYPE = "context.benchmark.Store2Key"; + private static final String STORE2_CONTEXT_TYPE = CONTEXT_TYPE + "2"; + private static final String STORE3_KEY_TYPE = "context.benchmark.Store3Key"; + private static final String STORE3_CONTEXT_TYPE = CONTEXT_TYPE + "3"; + + public ContextStoreBenchmarkInstrumentation() { + super("context-store-benchmark"); + } + + @Override + public String hierarchyMarkerType() { + return KEY_TYPE; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named(KEY_TYPE)); + } + + @Override + public Map contextStore() { + Map stores = new LinkedHashMap<>(); + stores.put(KEY_TYPE, CONTEXT_TYPE); + stores.put(STORE1_KEY_TYPE, STORE1_CONTEXT_TYPE); + stores.put(STORE2_KEY_TYPE, STORE2_CONTEXT_TYPE); + stores.put(STORE3_KEY_TYPE, STORE3_CONTEXT_TYPE); + return Collections.unmodifiableMap(stores); + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice(named("read"), getClass().getName() + "$ReadAdvice"); + transformer.applyAdvice(named("write"), getClass().getName() + "$WriteAdvice"); + transformer.applyAdvice(named("getContext"), getClass().getName() + "$GetContextAdvice"); + transformer.applyAdvice(named("putContext"), getClass().getName() + "$PutContextAdvice"); + transformer.applyAdvice(named("removeContext"), getClass().getName() + "$RemoveContextAdvice"); + transformer.applyAdvice( + named("getOrPutContext"), getClass().getName() + "$GetOrPutContextAdvice"); + transformer.applyAdvice( + named("getOrComputeContext"), getClass().getName() + "$GetOrComputeContextAdvice"); + } + + public static class State { + public final int value; + + public State(int value) { + this.value = value; + } + } + + public static final class State1 extends State { + public State1(int value) { + super(value); + } + } + + public static final class State2 extends State { + public State2(int value) { + super(value); + } + } + + public static final class State3 extends State { + public State3(int value) { + super(value); + } + } + + public static final class ReadAdvice { + @Advice.OnMethodExit + public static void exit(@Advice.This Object key, @Advice.Return(readOnly = false) int result) { + ContextStore store = InstrumentationContext.get(KEY_TYPE, CONTEXT_TYPE); + result = store.get(key).value; + } + } + + public static final class WriteAdvice { + @Advice.OnMethodExit + public static void exit(@Advice.This Object key, @Advice.Argument(0) Object context) { + ContextStore store = InstrumentationContext.get(KEY_TYPE, CONTEXT_TYPE); + store.put(key, (State) context); + } + } + + public static final class GetContextAdvice { + @Advice.OnMethodExit + public static void exit( + @Advice.This Object key, + @Advice.Argument(0) int storeIndex, + @Advice.Return(readOnly = false) Object context) { + ContextStore store; + switch (storeIndex) { + case 0: + store = InstrumentationContext.get(KEY_TYPE, CONTEXT_TYPE); + break; + case 1: + store = InstrumentationContext.get(STORE1_KEY_TYPE, STORE1_CONTEXT_TYPE); + break; + case 2: + store = InstrumentationContext.get(STORE2_KEY_TYPE, STORE2_CONTEXT_TYPE); + break; + case 3: + store = InstrumentationContext.get(STORE3_KEY_TYPE, STORE3_CONTEXT_TYPE); + break; + default: + throw new IllegalArgumentException("unsupported store index: " + storeIndex); + } + context = store.get(key); + } + } + + public static final class PutContextAdvice { + @Advice.OnMethodExit + public static void exit( + @Advice.This Object key, + @Advice.Argument(0) int storeIndex, + @Advice.Argument(1) Object context) { + ContextStore store; + switch (storeIndex) { + case 0: + store = InstrumentationContext.get(KEY_TYPE, CONTEXT_TYPE); + break; + case 1: + store = InstrumentationContext.get(STORE1_KEY_TYPE, STORE1_CONTEXT_TYPE); + break; + case 2: + store = InstrumentationContext.get(STORE2_KEY_TYPE, STORE2_CONTEXT_TYPE); + break; + case 3: + store = InstrumentationContext.get(STORE3_KEY_TYPE, STORE3_CONTEXT_TYPE); + break; + default: + throw new IllegalArgumentException("unsupported store index: " + storeIndex); + } + store.put(key, (State) context); + } + } + + public static final class RemoveContextAdvice { + @Advice.OnMethodExit + public static void exit( + @Advice.This Object key, + @Advice.Argument(0) int storeIndex, + @Advice.Return(readOnly = false) Object context) { + ContextStore store; + switch (storeIndex) { + case 0: + store = InstrumentationContext.get(KEY_TYPE, CONTEXT_TYPE); + break; + case 1: + store = InstrumentationContext.get(STORE1_KEY_TYPE, STORE1_CONTEXT_TYPE); + break; + case 2: + store = InstrumentationContext.get(STORE2_KEY_TYPE, STORE2_CONTEXT_TYPE); + break; + case 3: + store = InstrumentationContext.get(STORE3_KEY_TYPE, STORE3_CONTEXT_TYPE); + break; + default: + throw new IllegalArgumentException("unsupported store index: " + storeIndex); + } + context = store.remove(key); + } + } + + public static final class GetOrPutContextAdvice { + @Advice.OnMethodExit + public static void exit( + @Advice.This Object key, + @Advice.Argument(0) int storeIndex, + @Advice.Argument(1) Object context, + @Advice.Return(readOnly = false) Object result) { + ContextStore store; + switch (storeIndex) { + case 0: + store = InstrumentationContext.get(KEY_TYPE, CONTEXT_TYPE); + break; + case 1: + store = InstrumentationContext.get(STORE1_KEY_TYPE, STORE1_CONTEXT_TYPE); + break; + case 2: + store = InstrumentationContext.get(STORE2_KEY_TYPE, STORE2_CONTEXT_TYPE); + break; + case 3: + store = InstrumentationContext.get(STORE3_KEY_TYPE, STORE3_CONTEXT_TYPE); + break; + default: + throw new IllegalArgumentException("unsupported store index: " + storeIndex); + } + result = store.getOrPut(key, context); + } + } + + public static final class GetOrComputeContextAdvice { + @Advice.OnMethodExit + public static void exit( + @Advice.This Object key, + @Advice.Argument(0) int storeIndex, + @Advice.Argument(1) Function contextFactory, + @Advice.Return(readOnly = false) Object result) { + ContextStore store; + switch (storeIndex) { + case 0: + store = InstrumentationContext.get(KEY_TYPE, CONTEXT_TYPE); + break; + case 1: + store = InstrumentationContext.get(STORE1_KEY_TYPE, STORE1_CONTEXT_TYPE); + break; + case 2: + store = InstrumentationContext.get(STORE2_KEY_TYPE, STORE2_CONTEXT_TYPE); + break; + case 3: + store = InstrumentationContext.get(STORE3_KEY_TYPE, STORE3_CONTEXT_TYPE); + break; + default: + throw new IllegalArgumentException("unsupported store index: " + storeIndex); + } + result = store.getOrCompute(key, contextFactory); + } + } + + public static final class Muzzle { + public static ReferenceMatcher create() { + return ReferenceMatcher.NO_REFERENCES; + } + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreBenchmarkSupport.java b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreBenchmarkSupport.java new file mode 100644 index 00000000000..9a0ffe3a2bc --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreBenchmarkSupport.java @@ -0,0 +1,80 @@ +package datadog.trace.agent.tooling.context.benchmark; + +import static datadog.trace.api.config.TraceInstrumentationConfig.RUNTIME_CONTEXT_MAP_PER_STORE; + +import datadog.trace.agent.tooling.AgentInstaller; +import datadog.trace.api.InstrumenterConfig; +import datadog.trace.bootstrap.ObjectStoreCleaner; +import java.lang.instrument.Instrumentation; +import net.bytebuddy.agent.ByteBuddyAgent; + +/** Installs the agent once per JVM and creates keys for the context-store benchmarks. */ +final class ContextStoreBenchmarkSupport { + private static final String MAP_PER_STORE_PROPERTY = "dd." + RUNTIME_CONTEXT_MAP_PER_STORE; + + /** Key types that must not get injected fields, across all the context-store benchmarks. */ + private static final String[] UNINJECTED_KEY_TYPES = { + "context.benchmark.NonFieldInjectedKey", + "context.benchmark.workload.UninjectedKey", + "context.benchmark.workload.PartialKeyBase" + }; + + // static so the agent is only installed once per JVM, even when running with -f 0 + private static boolean installed; + private static boolean installedMapPerStore; + + private ContextStoreBenchmarkSupport() {} + + /** + * Installs the agent with the requested context-store mode, or the configured mode when {@code + * mapPerStore} is {@code null}. + * + * @return whether the installed agent uses a map per store + */ + static synchronized boolean installAgent(Boolean mapPerStore) throws Exception { + if (!installed) { + if (mapPerStore != null) { + // must be set before AgentInstaller or ContextStores read the config + System.setProperty(MAP_PER_STORE_PROPERTY, mapPerStore.toString()); + } + + // must load before installing the agent: retransformation can't add fields to a class + // that's already loaded, so these key types will fall back to the store's map + for (String type : UNINJECTED_KEY_TYPES) { + Class.forName(type); + } + + Instrumentation instrumentation = ByteBuddyAgent.install(); + AgentInstaller.installBytebuddyAgent(instrumentation); + // scheduled by Agent.start in production; a no-op when map-per-store is enabled + ObjectStoreCleaner.schedule(); + + installedMapPerStore = InstrumenterConfig.get().isRuntimeContextMapPerStore(); + installed = true; + } + if (mapPerStore != null && mapPerStore != installedMapPerStore) { + throw new IllegalStateException( + "map-per-store=" + + installedMapPerStore + + " is already installed, but " + + mapPerStore + + " was requested; run with forks"); + } + return installedMapPerStore; + } + + /** Creates a key of the given type, checking whether it has injected fields. */ + @SuppressWarnings("unchecked") + static K newKey(String typeName, boolean expectInjected) throws Exception { + Object key = Class.forName(typeName).getConstructor().newInstance(); + boolean injected = false; + for (Class type : key.getClass().getInterfaces()) { + injected |= type.getName().equals("datadog.instrument.fieldinject.KeyWithValue"); + } + if (injected != expectInjected) { + throw new IllegalStateException( + typeName + " field injection: expected " + expectInjected + ", got " + injected); + } + return (K) key; + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreWorkloadBenchmark.java b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreWorkloadBenchmark.java new file mode 100644 index 00000000000..5d3cb12598a --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreWorkloadBenchmark.java @@ -0,0 +1,776 @@ +package datadog.trace.agent.tooling.context.benchmark; + +import static datadog.trace.agent.tooling.context.benchmark.ContextStoreBenchmarkSupport.installAgent; +import static datadog.trace.agent.tooling.context.benchmark.ContextStoreBenchmarkSupport.newKey; +import static datadog.trace.agent.tooling.context.benchmark.ContextStoreWorkloadInstrumentation.STORE_COUNT; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import context.benchmark.workload.WorkloadKey; +import datadog.trace.agent.tooling.context.benchmark.ContextStoreWorkloadInstrumentation.Context; +import datadog.trace.bootstrap.ContextStore; +import java.util.Random; +import java.util.function.Function; +import org.openjdk.jmh.annotations.AuxCounters; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.BenchmarkParams; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.infra.IterationParams; +import org.openjdk.jmh.infra.ThreadParams; +import org.openjdk.jmh.runner.IterationType; + +/** + * Compares map-per-store settings using common instrumentation lifecycles. Each store has one fixed + * behaviour, selected by {@code scenario}: + * + *

+ * + *

16 stores have Zipf popularity. {@code fallbackPercent} controls the fraction of fallback + * traffic. {@code fallbackMode=DISTRIBUTED} spreads it across eligible stores; {@code CONCENTRATED} + * confines it to stores 14-15, so those stores' behaviours determine the fallback mix. Half of the + * traffic also visits a second store, which can have a different behaviour. Request-local stores + * share the new key; reuse stores select their own pooled key. Initial assignment uses {@code put} + * (50%), {@code getOrPut} (25%) or {@code getOrCompute} (25%). Values are fresh per key; + * recreations also allocate fresh values. Reuse stores have up to 64 keys per key kind per thread. + * + *

{@code *AssignmentMisses} counts contexts missing immediately after assignment. {@code + * *YoungRetirementMisses} counts contexts missing at request completion; {@code *PooledReuseMisses} + * counts contexts missing before a pooled key is reused. Divide by the respective {@code *Checks} + * counter for a miss rate. {@code *RevisitRecreates} counts recreation attempts, including rejected + * associations. Reuse revisits follow pooled checks; recovery revisits follow young retirement + * checks. Both make four visits per checked association; other lifecycles make none. Iteration + * teardown rejects injected misses or recreations, and requires one recreation per fallback + * association in recovery. Compare timings only when successful work and retention are comparable: + * full per-store maps can drop new associations and make that mode appear faster. + * + *

When analysing retention, always favour young state over existing pooled state. In production, + * many older objects linger in the heap after tracing has finished; future tracing may replace + * their values anyway. This benchmark actively revisits pooled state, giving its retention more + * weight than such lingering objects warrant. + * + *

Retained state competes with abandoned entries in mixed runs. Finishing a span or consuming a + * continuation does not necessarily remove its state from the store; reuse models that distinction. + * Recovery is explicit invalidation, not a model of natural eviction. + * + *

Each benchmark runs in its own forks with the agent's stale-entry cleaners. {@code + * requestWork} burns CPU per request to control churn. {@code baseline} follows the same key and + * value lifecycles without stores; compare raw request timings between modes. The request ring must + * be full, with at least {@code minFallbackRetirementChecks} fallback checks per measurement and + * thread. Seed and coverage overrides are diagnostic parameters. + * + *

Stores are selected through the {@link ContextStore} interface, whereas instrumentation + * normally uses a constant store. {@link ContextStoreBenchmark#constantStoreRequestLifecycle} + * complements this workload with literal store IDs. GC frequency depends on store allocations; use + * a fixed heap and compare collectors, JDKs, and one versus many threads. + * + *

+ * ./gradlew :dd-java-agent:agent-tooling:jmhJar
+ * java -jar dd-java-agent/agent-tooling/build/libs/agent-tooling-*-jmh.jar \
+ *   ContextStoreWorkloadBenchmark -t 16 -prof gc -jvmArgsAppend "-Xmx512m -XX:+UseG1GC"
+ * 
+ */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(NANOSECONDS) +@Warmup(iterations = 3, time = 1) +// spans several 1s stale-entry cleaner sweeps, so each iteration sees a similar amount of cleanup +@Measurement(iterations = 5, time = 3) +@Fork(2) +public class ContextStoreWorkloadBenchmark { + static final int INJECTED = 0; + static final int PARTIAL = 1; + static final int UNINJECTED = 2; + + static final int PUT = 0; + static final int GET_OR_PUT = 1; + static final int GET_OR_COMPUTE = 2; + + static final int KEYS_PER_REQUEST = 4; + static final int IN_FLIGHT_KEYS = 16 * KEYS_PER_REQUEST; // keys stay in flight for 16 requests + static final int SECOND_STORE_PERCENT = 50; + static final int PUT_PERCENT = 50; + static final int GET_OR_PUT_PERCENT = 25; + static final int REVISIT_COUNT = 4; + static final int PATTERN_SIZE = 65536; + + /** Stores selected exclusively for non-injected keys in CONCENTRATED mode. */ + static final int[] FALLBACK_STORES = {14, 15}; + + static final Context CONTEXT = new Context(42); + static final Function COMPUTE_CONTEXT = key -> CONTEXT; + + /** Cumulative Zipf (s=1) weights, so lower store indices are more popular. */ + static final double[] ZIPF = zipf(STORE_COUNT); + + public enum FallbackMode { + /** Fallback traffic is spread across eligible stores with Zipf popularity. */ + DISTRIBUTED, + /** Fallback traffic is confined to stores 14-15; injected traffic uses the other stores. */ + CONCENTRATED + } + + /** + * Request propagation (Jetty), explicit handoff/cleanup (Reactive Streams), and retained state + * (Netty/JMS), alone or mixed across stores. Recovery is a synthetic diagnostic. + */ + public enum Scenario { + GC, + CLEANUP, + REUSE, + MIXED, + CHURN_HEAVY, + REUSE_HEAVY, + RECOVERY; + + /** Returns the fixed lifecycle for a store. */ + Scenario forStore(int store) { + switch (this) { + case MIXED: + return store % 3 == 0 ? GC : store % 3 == 1 ? CLEANUP : REUSE; + case CHURN_HEAVY: + return store % 6 < 3 ? GC : store % 6 < 5 ? CLEANUP : REUSE; + case REUSE_HEAVY: + if (store == FALLBACK_STORES[1]) { + return GC; + } + return store % 6 < 4 ? REUSE : store % 6 == 4 ? GC : CLEANUP; + default: + return this; + } + } + } + + /** Runs the selected store lifecycles with {@code map-per-store=true}. */ + @Benchmark + public void requestMapPerStore(Workload workload, Misses misses, Blackhole blackhole) { + run(workload, misses.request, misses, true, blackhole); + } + + /** Same as {@link #requestMapPerStore} with {@code map-per-store=false}. */ + @Benchmark + public void requestGlobalStore(Workload workload, Misses misses, Blackhole blackhole) { + run(workload, misses.request, misses, true, blackhole); + } + + /** Same workload as the request benchmarks without touching the stores. */ + @Benchmark + public void baseline(Workload workload, Request request, Blackhole blackhole) { + run(workload, request, null, false, blackhole); + } + + private static void run( + Workload workload, Request request, Misses misses, boolean useStores, Blackhole blackhole) { + for (int k = 0; k < KEYS_PER_REQUEST; k++) { + int p = request.nextPattern(); + int slot = request.nextSlot(); + WorkloadKey finished = request.inFlightKeys[slot]; + if (finished != null && useStores) { + int q = request.inFlightPatterns[slot]; + request.finish(workload, finished, q, request.inFlightContexts[slot], misses, blackhole); + } + + boolean pooledA = workload.behaviours[request.storesA[p]] == Scenario.REUSE; + boolean pooledB = + request.storesB[p] < 0 || workload.behaviours[request.storesB[p]] == Scenario.REUSE; + WorkloadKey key = null; + Object context = null; + if (!pooledA || !pooledB) { + key = workload.prototypes[request.kinds[p]].newKey(); + context = new Context(42); + if (useStores) { + request.assign(workload, key, p, context); + misses.assigned( + request.kinds[p], + request.contextCount(workload, p), + request.check(workload, key, p, context)); + } + } + request.reuse(workload, request.storesA[p], p, slot, useStores, misses, blackhole); + if (request.storesB[p] >= 0) { + request.reuse(workload, request.storesB[p], p, slot, useStores, misses, blackhole); + } + request.inFlightKeys[slot] = key; + request.inFlightPatterns[slot] = p; + request.inFlightContexts[slot] = context; + } + + Blackhole.consumeCPU(workload.requestWork); + } + + @State(Scope.Benchmark) + public static class Workload { + @Param("1") + public long seed; + + @Param({"1", "10"}) + public int fallbackPercent; + + @Param({"DISTRIBUTED", "CONCENTRATED"}) + public FallbackMode fallbackMode; + + @Param({"GC", "CLEANUP", "REUSE", "MIXED"}) + public Scenario scenario; + + @Param("1000") + public long requestWork; + + /** Minimum fallback completion/reuse checks per thread and measurement iteration. */ + @Param("100") + public int minFallbackRetirementChecks; + + private final Scenario[] behaviours = new Scenario[STORE_COUNT]; + + private final WorkloadKey[] prototypes = new WorkloadKey[3]; + + @SuppressWarnings("unchecked") + private final ContextStore[] stores = new ContextStore[STORE_COUNT]; + + @Setup(Level.Trial) + @SuppressWarnings("unchecked") + public void setUp(BenchmarkParams benchmarkParams) throws Exception { + validateParameters(); + boolean mapPerStore = installAgent(requestedMode(benchmarkParams.getBenchmark())); + + prototypes[INJECTED] = newKey("context.benchmark.workload.InjectedKey", true); + prototypes[PARTIAL] = newKey("context.benchmark.workload.PartialKey", true); + prototypes[UNINJECTED] = newKey("context.benchmark.workload.UninjectedKey", false); + + for (int i = 0; i < STORE_COUNT; i++) { + behaviours[i] = scenario.forStore(i); + stores[i] = (ContextStore) prototypes[INJECTED].store(i); + if (stores[i] == null) { + throw new IllegalStateException("context store " + i + " is not active"); + } + } + String expectedStore = mapPerStore ? "FieldBacked" : "Boxed"; + if (!stores[0].getClass().getSimpleName().startsWith(expectedStore)) { + throw new IllegalStateException( + "mapPerStore=" + mapPerStore + " but stores are " + stores[0].getClass().getName()); + } + verifyStores(); + + if (!schedulerRunning()) { + throw new IllegalStateException("stale-entry cleaners are not scheduled"); + } + } + + /** Returns the mode for the given request benchmark, or {@code null} for the baseline. */ + private static Boolean requestedMode(String benchmark) { + if (benchmark.endsWith(".requestMapPerStore")) { + return Boolean.TRUE; + } else if (benchmark.endsWith(".requestGlobalStore")) { + return Boolean.FALSE; + } else if (benchmark.endsWith(".baseline")) { + return null; + } + throw new IllegalArgumentException("unknown context-store workload benchmark: " + benchmark); + } + + private void validateParameters() { + requirePercentage("fallbackPercent", fallbackPercent); + if (requestWork < 0) { + throw new IllegalArgumentException("requestWork must not be negative"); + } + if (minFallbackRetirementChecks < 0) { + throw new IllegalArgumentException("minFallbackRetirementChecks must not be negative"); + } + } + + /** Checks store isolation for every key type, including the partial-field redirect. */ + private void verifyStores() { + Context[] contexts = new Context[STORE_COUNT]; + for (int i = 0; i < STORE_COUNT; i++) { + contexts[i] = new Context(i); + } + for (int kind = INJECTED; kind <= UNINJECTED; kind++) { + WorkloadKey key = prototypes[kind].newKey(); + for (int i = 0; i < STORE_COUNT; i++) { + stores[i].put(key, contexts[i]); + } + for (int i = 0; i < STORE_COUNT; i++) { + if (stores[i].get(key) != contexts[i] + || stores[i].getOrPut(key, CONTEXT) != contexts[i] + || stores[i].getOrCompute(key, COMPUTE_CONTEXT) != contexts[i] + || stores[i].remove(key) != contexts[i]) { + throw new IllegalStateException( + "context store " + i + " is not isolated for " + key.getClass().getName()); + } + for (int j = 0; j < STORE_COUNT; j++) { + if (stores[j].get(key) != (j <= i ? null : contexts[j])) { + throw new IllegalStateException( + "removing context store " + i + " affected store " + j); + } + } + } + } + } + } + + @State(Scope.Thread) + public static class Request { + // bytes rather than ints keep the per-thread patterns small, so they disturb the cache less + private final byte[] kinds = new byte[PATTERN_SIZE]; + private final byte[] storesA = new byte[PATTERN_SIZE]; + private final byte[] storesB = new byte[PATTERN_SIZE]; + private final byte[] ops = new byte[PATTERN_SIZE]; + private final WorkloadKey[] inFlightKeys = new WorkloadKey[IN_FLIGHT_KEYS]; + private final int[] inFlightPatterns = new int[IN_FLIGHT_KEYS]; + private final Object[] inFlightContexts = new Object[IN_FLIGHT_KEYS]; + private Object assignmentContext; + // Reused by this thread; getOrCompute invokes it synchronously, without a per-key lambda. + private final Function contextFactory = key -> assignmentContext; + private final WorkloadKey[][][] pooledKeys = new WorkloadKey[STORE_COUNT][3][IN_FLIGHT_KEYS]; + private final Object[][][] pooledContexts = new Object[STORE_COUNT][3][IN_FLIGHT_KEYS]; + private long revisitRecreates; + // Only revisit misses count as recreations. + private final Function revisitFactory = key -> recreateContext(); + private int patternIndex; + private int slotIndex; + private int youngOccupiedSlots; + + @Setup(Level.Trial) + public void setUp(Workload workload, ThreadParams threadParams) { + // spread seeds so different seeds don't reuse each other's per-thread patterns + Random random = new Random(workload.seed * 1_000_003L + threadParams.getThreadIndex()); + boolean concentrated = workload.fallbackMode == FallbackMode.CONCENTRATED; + int fallbackCount = (int) Math.round(PATTERN_SIZE * workload.fallbackPercent / 100.0); + int partialCount = concentrated ? 0 : fallbackCount / 2; + int[] counts = {PATTERN_SIZE - fallbackCount, partialCount, fallbackCount - partialCount}; + int offset = 0; + for (int kind = INJECTED; kind <= UNINJECTED; kind++) { + for (int i = 0; i < counts[kind]; i++) { + kinds[offset++] = (byte) kind; + } + } + // Shuffle key kinds to retain fallback coverage without clustering requests. + for (int p = PATTERN_SIZE - 1; p > 0; p--) { + int q = random.nextInt(p + 1); + byte kind = kinds[p]; + kinds[p] = kinds[q]; + kinds[q] = kind; + } + for (int p = 0; p < PATTERN_SIZE; p++) { + int kind = kinds[p]; + storesA[p] = (byte) pickStore(random, kind, concentrated); + storesB[p] = + (byte) + (random.nextInt(100) < SECOND_STORE_PERCENT + ? pickOtherStore(random, kind, concentrated, storesA[p]) + : -1); + int op = random.nextInt(100); + ops[p] = + (byte) + (op < PUT_PERCENT + ? PUT + : op < PUT_PERCENT + GET_OR_PUT_PERCENT ? GET_OR_PUT : GET_OR_COMPUTE); + } + } + + /** Assigns contexts to the request-local stores for pattern {@code p}. */ + void assign(Workload workload, WorkloadKey key, int p, Object context) { + assignmentContext = context; + if (workload.behaviours[storesA[p]] != Scenario.REUSE) { + assign(workload.stores[storesA[p]], key, ops[p], context, contextFactory); + } + if (storesB[p] >= 0 && workload.behaviours[storesB[p]] != Scenario.REUSE) { + assign(workload.stores[storesB[p]], key, ops[p], context, contextFactory); + } + } + + private static void assign( + ContextStore store, + WorkloadKey key, + int op, + Object context, + Function factory) { + switch (op) { + case GET_OR_PUT: + store.getOrPut(key, context); + break; + case GET_OR_COMPUTE: + store.getOrCompute(key, factory); + break; + default: + store.put(key, context); + } + } + + /** Returns how many request-local contexts for pattern {@code p} are missing. */ + int check(Workload workload, WorkloadKey key, int p, Object context) { + int missing = 0; + if (workload.behaviours[storesA[p]] != Scenario.REUSE) { + missing += workload.stores[storesA[p]].get(key) != context ? 1 : 0; + } + if (storesB[p] >= 0 && workload.behaviours[storesB[p]] != Scenario.REUSE) { + missing += workload.stores[storesB[p]].get(key) != context ? 1 : 0; + } + return missing; + } + + int contextCount(Workload workload, int p) { + return (workload.behaviours[storesA[p]] == Scenario.REUSE ? 0 : 1) + + (storesB[p] < 0 || workload.behaviours[storesB[p]] == Scenario.REUSE ? 0 : 1); + } + + /** Checks request-local associations before their keys leave flight. */ + void finish( + Workload workload, + WorkloadKey key, + int p, + Object context, + Misses misses, + Blackhole blackhole) { + misses.retired(kinds[p], contextCount(workload, p), check(workload, key, p, context)); + finish(workload, storesA[p], key, p, misses, blackhole); + if (storesB[p] >= 0) { + finish(workload, storesB[p], key, p, misses, blackhole); + } + } + + private void finish( + Workload workload, int index, WorkloadKey key, int p, Misses misses, Blackhole blackhole) { + Scenario behaviour = workload.behaviours[index]; + ContextStore store = workload.stores[index]; + if (behaviour == Scenario.RECOVERY) { + if (kinds[p] != INJECTED) { + store.remove(key); + } + revisit(store, key, kinds[p], misses, blackhole); + } + if (behaviour == Scenario.CLEANUP || behaviour == Scenario.RECOVERY) { + store.remove(key); + } + } + + /** Uses a persistent key from the selected store's pool, repairing missing state. */ + void reuse( + Workload workload, + int index, + int p, + int slot, + boolean useStores, + Misses misses, + Blackhole blackhole) { + if (workload.behaviours[index] != Scenario.REUSE) { + return; + } + int kind = kinds[p]; + WorkloadKey key = pooledKeys[index][kind][slot]; + Object context = pooledContexts[index][kind][slot]; + ContextStore store = workload.stores[index]; + if (key == null) { + key = workload.prototypes[kind].newKey(); + context = new Context(42); + pooledKeys[index][kind][slot] = key; + if (useStores) { + assignmentContext = context; + assign(store, key, ops[p], context, contextFactory); + misses.assigned(kind, 1, store.get(key) != context ? 1 : 0); + } + } else if (useStores) { + misses.reused(kind, store.get(key) != context ? 1 : 0); + context = revisit(store, key, kind, misses, blackhole); + } + pooledContexts[index][kind][slot] = context; + } + + /** Revisits an association and records any recreations. */ + private Object revisit( + ContextStore store, + WorkloadKey key, + int kind, + Misses misses, + Blackhole blackhole) { + long before = revisitRecreates; + Object context = null; + for (int visit = 0; visit < REVISIT_COUNT; visit++) { + context = revisit(store, key, (visit & 1) == 0); + blackhole.consume(context); + } + misses.revisited(kind, revisitRecreates - before); + return context; + } + + /** Revisits an association, recreating its value only when missing. */ + private Object revisit(ContextStore store, WorkloadKey key, boolean compute) { + if (compute) { + return store.getOrCompute(key, revisitFactory); + } + Object context = store.get(key); + return store.getOrPut(key, context != null ? context : recreateContext()); + } + + private Object recreateContext() { + revisitRecreates++; + return new Context(42); + } + + int nextPattern() { + int p = patternIndex; + patternIndex = (p + 1) & (PATTERN_SIZE - 1); + return p; + } + + int nextSlot() { + int i = slotIndex; + slotIndex = (i + 1) & (IN_FLIGHT_KEYS - 1); + // Pooled-only traffic fills scheduling slots without adding request-local keys. + if (youngOccupiedSlots < IN_FLIGHT_KEYS) { + youngOccupiedSlots++; + } + return i; + } + } + + /** + * Context checks and misses, split by whether the contexts are field-injected or fall back, and + * by how recently they were assigned. Partially injected keys count as fallback because this + * workload only selects stores for which they have no fields; see {@link #pickStore}. + */ + @State(Scope.Thread) + @AuxCounters(AuxCounters.Type.EVENTS) + public static class Misses { + private Request request; + private int minFallbackRetirementChecks; + private boolean hasFallback; + private boolean recovery; + + /** Request slots occupied at the end of the iteration, including pooled keys. */ + public long youngOccupiedSlots() { + return request.youngOccupiedSlots; + } + + /** Request slots available per thread. */ + public long youngCapacitySlots() { + return request.inFlightKeys.length; + } + + /** Injected contexts checked straight after being assigned. */ + public long injectedAssignmentChecks; + + /** Injected contexts missing straight after being assigned. */ + public long injectedAssignmentMisses; + + /** Young injected contexts checked at completion. */ + public long injectedYoungRetirementChecks; + + /** Young injected contexts missing at completion. */ + public long injectedYoungRetirementMisses; + + /** Pooled injected contexts checked before reuse. */ + public long injectedPooledReuseChecks; + + /** Pooled injected contexts missing before reuse. */ + public long injectedPooledReuseMisses; + + /** Injected contexts needing recreation during revisits. */ + public long injectedRevisitRecreates; + + /** Fallback contexts checked straight after being assigned. */ + public long fallbackAssignmentChecks; + + /** Fallback contexts missing straight after being assigned. */ + public long fallbackAssignmentMisses; + + /** Young fallback contexts checked at completion. */ + public long fallbackYoungRetirementChecks; + + /** Young fallback contexts missing at completion. */ + public long fallbackYoungRetirementMisses; + + /** Pooled fallback contexts checked before reuse. */ + public long fallbackPooledReuseChecks; + + /** Pooled fallback contexts missing before reuse. */ + public long fallbackPooledReuseMisses; + + /** Fallback contexts needing recreation during revisits. */ + public long fallbackRevisitRecreates; + + @Setup(Level.Iteration) + public void reset(Request request, Workload workload) { + this.request = request; + minFallbackRetirementChecks = workload.minFallbackRetirementChecks; + hasFallback = workload.fallbackPercent > 0; + recovery = workload.scenario == Scenario.RECOVERY; + injectedAssignmentChecks = 0; + injectedAssignmentMisses = 0; + injectedYoungRetirementChecks = 0; + injectedYoungRetirementMisses = 0; + injectedPooledReuseChecks = 0; + injectedPooledReuseMisses = 0; + injectedRevisitRecreates = 0; + fallbackAssignmentChecks = 0; + fallbackAssignmentMisses = 0; + fallbackYoungRetirementChecks = 0; + fallbackYoungRetirementMisses = 0; + fallbackPooledReuseChecks = 0; + fallbackPooledReuseMisses = 0; + fallbackRevisitRecreates = 0; + } + + @TearDown(Level.Iteration) + public void verifyIteration(IterationParams iteration) { + if (injectedAssignmentMisses != 0 + || injectedYoungRetirementMisses != 0 + || injectedPooledReuseMisses != 0 + || injectedRevisitRecreates != 0) { + throw new IllegalStateException( + "Injected state was lost: assignment misses=" + + injectedAssignmentMisses + + ", young misses=" + + injectedYoungRetirementMisses + + ", pooled misses=" + + injectedPooledReuseMisses + + ", recreations=" + + injectedRevisitRecreates); + } + if (recovery && fallbackRevisitRecreates != fallbackYoungRetirementChecks) { + throw new IllegalStateException( + "Recovery requires one recreation per fallback association: checks=" + + fallbackYoungRetirementChecks + + ", recreations=" + + fallbackRevisitRecreates); + } + if (iteration.getType() != IterationType.MEASUREMENT) { + return; + } + if (request.youngOccupiedSlots != request.inFlightKeys.length) { + throw new IllegalStateException("Request ring is not full; increase -wi/-w"); + } + long fallbackChecks = fallbackYoungRetirementChecks + fallbackPooledReuseChecks; + if (hasFallback && fallbackChecks < minFallbackRetirementChecks) { + throw new IllegalStateException( + "Only " + + fallbackChecks + + " fallback completion/reuse checks; require " + + minFallbackRetirementChecks + + " per thread. Increase -wi/-w or -r; use -p minFallbackRetirementChecks=0 for smoke tests."); + } + } + + void assigned(int kind, int checks, int missing) { + if (kind != INJECTED) { + fallbackAssignmentChecks += checks; + fallbackAssignmentMisses += missing; + } else { + injectedAssignmentChecks += checks; + injectedAssignmentMisses += missing; + } + } + + void retired(int kind, int checks, int missing) { + if (kind != INJECTED) { + fallbackYoungRetirementChecks += checks; + fallbackYoungRetirementMisses += missing; + } else { + injectedYoungRetirementChecks += checks; + injectedYoungRetirementMisses += missing; + } + } + + void reused(int kind, int missing) { + if (kind != INJECTED) { + fallbackPooledReuseChecks++; + fallbackPooledReuseMisses += missing; + } else { + injectedPooledReuseChecks++; + injectedPooledReuseMisses += missing; + } + } + + void revisited(int kind, long recreates) { + if (kind != INJECTED) { + fallbackRevisitRecreates += recreates; + } else { + injectedRevisitRecreates += recreates; + } + } + } + + /** + * Picks a store by popularity. In CONCENTRATED mode non-injected keys only use the fallback + * stores and injected keys only use the others; otherwise partially injected keys only use stores + * 8-15, which they have no fields for. + */ + static int pickStore(Random random, int kind, boolean concentrated) { + int store; + do { + double x = random.nextDouble(); + store = 0; + while (ZIPF[store] < x) { + store++; + } + } while (concentrated + ? (kind == UNINJECTED) != isFallbackStore(store) + : kind == PARTIAL && store < STORE_COUNT / 2); + return store; + } + + static boolean isFallbackStore(int store) { + for (int fallbackStore : FALLBACK_STORES) { + if (store == fallbackStore) { + return true; + } + } + return false; + } + + static double[] zipf(int count) { + double[] cumulative = new double[count]; + double total = 0; + for (int i = 0; i < count; i++) { + total += 1.0 / (i + 1); + cumulative[i] = total; + } + for (int i = 0; i < count; i++) { + cumulative[i] /= total; + } + cumulative[count - 1] = 1.0; // guard against rounding + return cumulative; + } + + static int pickOtherStore(Random random, int kind, boolean concentrated, int store) { + int other; + do { + other = pickStore(random, kind, concentrated); + } while (other == store); + return other; + } + + static boolean schedulerRunning() { + for (Thread thread : Thread.getAllStackTraces().keySet()) { + if (thread.isAlive() && thread.getName().equals("dd-task-scheduler")) { + return true; + } + } + return false; + } + + private static void requirePercentage(String name, int value) { + if (value < 0 || value > 100) { + throw new IllegalArgumentException(name + " must be between 0 and 100"); + } + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreWorkloadInstrumentation.java b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreWorkloadInstrumentation.java new file mode 100644 index 00000000000..957faae2f36 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/jmh/java/datadog/trace/agent/tooling/context/benchmark/ContextStoreWorkloadInstrumentation.java @@ -0,0 +1,126 @@ +package datadog.trace.agent.tooling.context.benchmark; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; + +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.agent.tooling.muzzle.ReferenceMatcher; +import datadog.trace.bootstrap.InstrumentationContext; +import java.util.LinkedHashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** Declares 16 context stores and exposes them through {@code WorkloadKey.store(int)}. */ +public final class ContextStoreWorkloadInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { + static final int STORE_COUNT = 16; + + private static final String KEY_TYPE = "context.benchmark.workload.WorkloadKey"; + private static final String STORE_KEY_TYPE = "context.benchmark.workload.StoreKeys$S"; + private static final String CONTEXT_TYPE = + "datadog.trace.agent.tooling.context.benchmark.ContextStoreWorkloadInstrumentation$Context"; + + public ContextStoreWorkloadInstrumentation() { + super("context-store-workload-benchmark"); + } + + @Override + public String hierarchyMarkerType() { + return KEY_TYPE; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named(KEY_TYPE)); + } + + @Override + public Map contextStore() { + Map stores = new LinkedHashMap<>(); + for (int i = 0; i < STORE_COUNT; i++) { + stores.put(STORE_KEY_TYPE + i, CONTEXT_TYPE); + } + return stores; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice(named("store"), getClass().getName() + "$StoreAdvice"); + } + + public static final class Context { + public final int value; + + public Context(int value) { + this.value = value; + } + } + + public static final class StoreAdvice { + @Advice.OnMethodExit + public static void exit( + @Advice.Argument(0) int storeIndex, @Advice.Return(readOnly = false) Object store) { + switch (storeIndex) { + case 0: + store = InstrumentationContext.get(STORE_KEY_TYPE + "0", CONTEXT_TYPE); + break; + case 1: + store = InstrumentationContext.get(STORE_KEY_TYPE + "1", CONTEXT_TYPE); + break; + case 2: + store = InstrumentationContext.get(STORE_KEY_TYPE + "2", CONTEXT_TYPE); + break; + case 3: + store = InstrumentationContext.get(STORE_KEY_TYPE + "3", CONTEXT_TYPE); + break; + case 4: + store = InstrumentationContext.get(STORE_KEY_TYPE + "4", CONTEXT_TYPE); + break; + case 5: + store = InstrumentationContext.get(STORE_KEY_TYPE + "5", CONTEXT_TYPE); + break; + case 6: + store = InstrumentationContext.get(STORE_KEY_TYPE + "6", CONTEXT_TYPE); + break; + case 7: + store = InstrumentationContext.get(STORE_KEY_TYPE + "7", CONTEXT_TYPE); + break; + case 8: + store = InstrumentationContext.get(STORE_KEY_TYPE + "8", CONTEXT_TYPE); + break; + case 9: + store = InstrumentationContext.get(STORE_KEY_TYPE + "9", CONTEXT_TYPE); + break; + case 10: + store = InstrumentationContext.get(STORE_KEY_TYPE + "10", CONTEXT_TYPE); + break; + case 11: + store = InstrumentationContext.get(STORE_KEY_TYPE + "11", CONTEXT_TYPE); + break; + case 12: + store = InstrumentationContext.get(STORE_KEY_TYPE + "12", CONTEXT_TYPE); + break; + case 13: + store = InstrumentationContext.get(STORE_KEY_TYPE + "13", CONTEXT_TYPE); + break; + case 14: + store = InstrumentationContext.get(STORE_KEY_TYPE + "14", CONTEXT_TYPE); + break; + case 15: + store = InstrumentationContext.get(STORE_KEY_TYPE + "15", CONTEXT_TYPE); + break; + default: + throw new IllegalArgumentException("unsupported store index: " + storeIndex); + } + } + } + + public static final class Muzzle { + public static ReferenceMatcher create() { + return ReferenceMatcher.NO_REFERENCES; + } + } +} diff --git a/dd-java-agent/agent-tooling/src/jmh/resources/META-INF/services/datadog.trace.agent.tooling.InstrumenterModule b/dd-java-agent/agent-tooling/src/jmh/resources/META-INF/services/datadog.trace.agent.tooling.InstrumenterModule index 98c9cad1ee5..89e4e878fd8 100644 --- a/dd-java-agent/agent-tooling/src/jmh/resources/META-INF/services/datadog.trace.agent.tooling.InstrumenterModule +++ b/dd-java-agent/agent-tooling/src/jmh/resources/META-INF/services/datadog.trace.agent.tooling.InstrumenterModule @@ -1,2 +1,4 @@ datadog.trace.agent.tooling.bytebuddy.csi.CalleeBenchmarkInstrumentation datadog.trace.agent.tooling.bytebuddy.csi.CallSiteBenchmarkInstrumentation +datadog.trace.agent.tooling.context.benchmark.ContextStoreBenchmarkInstrumentation +datadog.trace.agent.tooling.context.benchmark.ContextStoreWorkloadInstrumentation diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/MemoizedMatchers.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/MemoizedMatchers.java index 0cdffbcc854..81aa62d0821 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/MemoizedMatchers.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/MemoizedMatchers.java @@ -7,7 +7,7 @@ import static datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer.MatcherKind.INTERFACE; import static datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer.MatcherKind.METHOD; import static datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer.MatcherKind.TYPE; -import static datadog.trace.bootstrap.FieldBackedContextStores.getContextStoreId; +import static datadog.trace.bootstrap.ContextStores.getContextStoreId; import datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers; import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter; diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextInjector.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextInjector.java index 43721b75872..6a04766ae45 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextInjector.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextInjector.java @@ -1,6 +1,6 @@ package datadog.trace.agent.tooling.context; -import static datadog.trace.bootstrap.FieldBackedContextStores.getContextStoreId; +import static datadog.trace.bootstrap.ContextStores.getContextStoreId; import static datadog.trace.util.Strings.getInternalName; import datadog.instrument.fieldinject.KeyWithValue; @@ -204,7 +204,7 @@ public void visitEnd() { if (!foundField) { addStoreField(); } - // first injector to reach here is responsible for adding the generated getter and setter + // first injector to reach here is responsible for adding the generated getter and putter // for the class - at this point all the other injectors will have recorded their requests final BitSet injectedStoreIds = getInjectedContextStores(); if (null != injectedStoreIds) { diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextRequestRewriter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextRequestRewriter.java index 08d52f9bd57..edbb757b9e0 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextRequestRewriter.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextRequestRewriter.java @@ -1,11 +1,10 @@ package datadog.trace.agent.tooling.context; -import static datadog.trace.bootstrap.FieldBackedContextStores.getContextStoreId; +import static datadog.trace.bootstrap.ContextStores.getContextStoreId; import static datadog.trace.util.Strings.getInternalName; import datadog.trace.bootstrap.ContextStore; -import datadog.trace.bootstrap.FieldBackedContextStore; -import datadog.trace.bootstrap.FieldBackedContextStores; +import datadog.trace.bootstrap.ContextStores; import datadog.trace.bootstrap.InstrumentationContext; import java.util.Map; import net.bytebuddy.asm.AsmVisitorWrapper; @@ -35,8 +34,8 @@ public final class FieldBackedContextRequestRewriter implements AsmVisitorWrappe static final String INSTRUMENTATION_CONTEXT_CLASS = getInternalName(InstrumentationContext.class.getName()); - static final String FIELD_BACKED_CONTEXT_STORES_CLASS = - getInternalName(FieldBackedContextStores.class.getName()); + static final String STORES_CLASS = + Type.getType(ContextStores.STORES_DESCRIPTOR).getInternalName(); static final String GET_METHOD = "get"; static final String GET_METHOD_DESCRIPTOR = @@ -46,14 +45,11 @@ public final class FieldBackedContextRequestRewriter implements AsmVisitorWrappe Type.getMethodDescriptor( Type.getType(ContextStore.class), Type.getType(String.class), Type.getType(String.class)); - static final String GET_CONTENT_STORE_METHOD = "getContextStore"; - static final String GET_CONTENT_STORE_METHOD_DESCRIPTOR = - Type.getMethodDescriptor(Type.getType(FieldBackedContextStore.class), Type.INT_TYPE); + static final String GET_CONTEXT_STORE_METHOD = "getContextStore"; + static final String GET_CONTEXT_STORE_METHOD_DESCRIPTOR = + Type.getMethodDescriptor(Type.getType(ContextStores.STORE_DESCRIPTOR), Type.INT_TYPE); - static final String FIELD_BACKED_CONTENT_STORE_DESCRIPTOR = - Type.getDescriptor(FieldBackedContextStore.class); - - static final String FAST_CONTENT_STORE_PREFIX = "contextStore"; + static final String FAST_STORE_PREFIX = "contextStore"; final Map contextStore; final String instrumenterClassName; @@ -151,19 +147,19 @@ public void visitMethodInsn( int storeId = getContextStoreId(keyClassName, contextClassName); // use fast direct field access for a small number of stores - if (storeId < FieldBackedContextStores.FAST_STORE_ID_LIMIT) { + if (storeId < ContextStores.FAST_STORE_ID_LIMIT) { mv.visitFieldInsn( Opcodes.GETSTATIC, - FIELD_BACKED_CONTEXT_STORES_CLASS, - FAST_CONTENT_STORE_PREFIX + storeId, - FIELD_BACKED_CONTENT_STORE_DESCRIPTOR); + STORES_CLASS, + FAST_STORE_PREFIX + storeId, + ContextStores.STORE_DESCRIPTOR); } else { mv.visitLdcInsn(storeId); mv.visitMethodInsn( Opcodes.INVOKESTATIC, - FIELD_BACKED_CONTEXT_STORES_CLASS, - GET_CONTENT_STORE_METHOD, - GET_CONTENT_STORE_METHOD_DESCRIPTOR, + STORES_CLASS, + GET_CONTEXT_STORE_METHOD, + GET_CONTEXT_STORE_METHOD_DESCRIPTOR, false); } } else { diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/ShouldInjectFieldsState.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/ShouldInjectFieldsState.java index 3e6f04d7ef3..3f690c74335 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/ShouldInjectFieldsState.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/ShouldInjectFieldsState.java @@ -1,6 +1,6 @@ package datadog.trace.agent.tooling.context; -import static datadog.trace.bootstrap.FieldBackedContextStores.getContextStoreId; +import static datadog.trace.bootstrap.ContextStores.getContextStoreId; import java.util.BitSet; import java.util.HashMap; diff --git a/dd-java-agent/instrumentation-testing/src/test/groovy/excludefilter/ExcludeFilterForkedTest.groovy b/dd-java-agent/instrumentation-testing/src/test/groovy/excludefilter/ExcludeFilterForkedTest.groovy index 243b6bc7549..c931cb956e5 100644 --- a/dd-java-agent/instrumentation-testing/src/test/groovy/excludefilter/ExcludeFilterForkedTest.groovy +++ b/dd-java-agent/instrumentation-testing/src/test/groovy/excludefilter/ExcludeFilterForkedTest.groovy @@ -1,8 +1,8 @@ package excludefilter import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.bootstrap.ContextStores import datadog.trace.config.inversion.ConfigHelper -import datadog.trace.bootstrap.FieldBackedContextStores import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter import java.lang.reflect.Field @@ -63,7 +63,7 @@ class ExcludeFilterForkedTest extends InstrumentationSpecification { private final boolean hasAccessorInterface InjectionCheck(Class clazz, Class key, Class value) { - int storeId = FieldBackedContextStores.getContextStoreId(key.name, value.name) + int storeId = ContextStores.getContextStoreId(key.name, value.name) String fieldName = "__datadogContext\$${storeId}" boolean hasField = false for (Field field : clazz.getDeclaredFields()) { diff --git a/dd-java-agent/instrumentation/graal/graal-native-image-20.0/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java b/dd-java-agent/instrumentation/graal/graal-native-image-20.0/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java index 8c8c6c06cb0..76381981173 100644 --- a/dd-java-agent/instrumentation/graal/graal-native-image-20.0/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java +++ b/dd-java-agent/instrumentation/graal/graal-native-image-20.0/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java @@ -56,8 +56,21 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[ args[oldLength++] = "-H:ReflectionConfigurationResources=" + "META-INF/native-image/com.datadoghq/dd-java-agent/reflect-config.json"; + + String contextStoresInitialization = + InstrumenterConfig.get().isRuntimeContextMapPerStore() + ? "datadog.trace.bootstrap.FieldBackedContextStores:build_time," + + "datadog.trace.bootstrap.FieldBackedContextStore:build_time," + + "datadog.trace.bootstrap.WeakMapPerStore:build_time," + + "datadog.trace.agent.tooling.WeakMaps$Adapter:build_time," + + "com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap:build_time," + + "com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap$1:build_time," + : "datadog.trace.bootstrap.BoxedContextStores:build_time," + + "datadog.trace.bootstrap.BoxedContextStore:build_time,"; + args[oldLength++] = "-H:ClassInitialization=" + + contextStoresInitialization + "com.datadog.profiling.controller.openjdk.events.AvailableProcessorCoresEvent:build_time," + "com.datadog.profiling.controller.openjdk.events.DeadlockEvent:build_time," + "com.datadog.profiling.controller.openjdk.events.ProfilerSettingEvent:build_time," @@ -70,8 +83,10 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[ + "datadog.environment.JavaVirtualMachine:rerun," + "datadog.environment.OperatingSystem:rerun," + "datadog.environment.OperatingSystem$Architecture:rerun," + + "datadog.instrument.fieldinject.GlobalObjectStore:build_time," + + "datadog.instrument.fieldinject.GlobalObjectStore$LookupKey:build_time," + + "datadog.instrument.fieldinject.GlobalObjectStore$LookupKeyCache:build_time," + "datadog.trace.agent.core.tagprocessor.TagsPostProcessorFactory$Lazy:run_time," - + "datadog.trace.agent.tooling.WeakMaps$Adapter:build_time," + "datadog.trace.api.Config:rerun," + "datadog.trace.api.Platform:rerun," + "datadog.trace.api.Platform$Captured:build_time," @@ -127,8 +142,6 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[ + "datadog.trace.bootstrap.ContextStore$Factory:build_time," + "datadog.trace.bootstrap.DatadogClassLoader:build_time," + "datadog.trace.bootstrap.InstrumentationClassLoader:build_time," - + "datadog.trace.bootstrap.FieldBackedContextStores:build_time," - + "datadog.trace.bootstrap.FieldBackedContextStore:build_time," + "datadog.trace.bootstrap.benchmark.StaticEventLogger:build_time," + "datadog.trace.bootstrap.blocking.BlockingExceptionHandler:build_time," + "datadog.trace.bootstrap.InstrumentationErrors:build_time," @@ -148,7 +161,6 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[ + "datadog.trace.bootstrap.instrumentation.jfr.exceptions.ExceptionSampleEvent:build_time," + "datadog.trace.bootstrap.instrumentation.jfr.backpressure.BackpressureSampleEvent:build_time," + "datadog.trace.bootstrap.instrumentation.jfr.directallocation.DirectAllocationTotalEvent:build_time," - + "datadog.trace.bootstrap.WeakMapPerStore:build_time," + "datadog.trace.config.inversion.ConfigHelper:rerun," + "datadog.trace.config.inversion.ConfigHelper$StrictnessPolicy:rerun," + "datadog.trace.config.inversion.GeneratedSupportedConfigurations:build_time," @@ -175,11 +187,6 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[ + "datadog.slf4j.helpers.SubstituteLoggerFactory:build_time," + "datadog.slf4j.impl.StaticLoggerBinder:build_time," + "datadog.slf4j.LoggerFactory:build_time," - + "datadog.instrument.fieldinject.GlobalObjectStore:build_time," - + "datadog.instrument.fieldinject.GlobalObjectStore$LookupKey:build_time," - + "datadog.instrument.fieldinject.GlobalObjectStore$LookupKeyCache:build_time," - + "com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap:build_time," - + "com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap$1:build_time," + "net.bytebuddy:build_time," + "com.sun.proxy:build_time," + "jnr.enxio.channels:run_time," diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/IastConnectionCallSite.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/IastConnectionCallSite.java index 40a1c839bde..e21f50f70b9 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/IastConnectionCallSite.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/IastConnectionCallSite.java @@ -1,7 +1,7 @@ package datadog.trace.instrumentation.jdbc; -import static datadog.trace.bootstrap.FieldBackedContextStores.getContextStore; -import static datadog.trace.bootstrap.FieldBackedContextStores.getContextStoreId; +import static datadog.trace.bootstrap.ContextStores.getContextStore; +import static datadog.trace.bootstrap.ContextStores.getContextStoreId; import datadog.trace.agent.tooling.csi.CallSite; import datadog.trace.api.iast.IastCallSites;