diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/decorator/TestDecorator.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/decorator/TestDecorator.java
index 91f3c5e8101..7236e8c823f 100644
--- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/decorator/TestDecorator.java
+++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/decorator/TestDecorator.java
@@ -4,6 +4,7 @@
public interface TestDecorator {
String TEST_TYPE = "test";
+ String TEST_TYPE_BENCHMARK = "benchmark";
AgentSpan afterStart(final AgentSpan span);
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/build.gradle b/dd-java-agent/instrumentation/jmh/jmh-1.0/build.gradle
new file mode 100644
index 00000000000..a5256396007
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/build.gradle
@@ -0,0 +1,23 @@
+plugins {
+ id 'dd-trace-java.instrumentation.testing-framework-tests'
+}
+
+apply from: "$rootDir/gradle/java.gradle"
+
+muzzle {
+ pass {
+ group = 'org.openjdk.jmh'
+ module = 'jmh-core'
+ versions = '[1.0,)'
+ }
+}
+
+dependencies {
+ compileOnly group: 'org.openjdk.jmh', name: 'jmh-core', version: '1.0'
+
+ testImplementation group: 'org.openjdk.jmh', name: 'jmh-core', version: '1.37'
+ testImplementation project(':dd-java-agent:agent-ci-visibility:civisibility-instrumentation-test-fixtures')
+
+ // JMH annotation processor generates META-INF/BenchmarkList from @Benchmark annotations
+ testAnnotationProcessor group: 'org.openjdk.jmh', name: 'jmh-generator-annprocess', version: '1.37'
+}
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/main/java/datadog/trace/instrumentation/jmh/DDOutputFormat.java b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/main/java/datadog/trace/instrumentation/jmh/DDOutputFormat.java
new file mode 100644
index 00000000000..9b94cb69d87
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/main/java/datadog/trace/instrumentation/jmh/DDOutputFormat.java
@@ -0,0 +1,214 @@
+package datadog.trace.instrumentation.jmh;
+
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_ERROR;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_FORKS;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_ITERATIONS;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_MAX;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_MIN;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_MODE;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_P50;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_P90;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_P95;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_P99;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_SAMPLE_COUNT;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_THREADS;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_TIME_UNIT;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_UNIT;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_VALUE;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.BENCHMARK_WARMUP_ITERATIONS;
+
+import datadog.trace.api.civisibility.InstrumentationBridge;
+import datadog.trace.api.civisibility.config.TestSourceData;
+import datadog.trace.api.civisibility.events.TestEventsHandler;
+import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
+import java.util.Collections;
+import org.openjdk.jmh.infra.BenchmarkParams;
+import org.openjdk.jmh.results.BenchmarkResult;
+import org.openjdk.jmh.results.Result;
+import org.openjdk.jmh.results.RunResult;
+import org.openjdk.jmh.runner.format.OutputFormat;
+import org.openjdk.jmh.util.Statistics;
+
+/**
+ * Wraps a JMH {@link OutputFormat} to emit CI Visibility spans for each benchmark method.
+ *
+ *
Hooks only fire once per benchmark method (after all forks and iterations complete), so there
+ * is zero overhead on the benchmark hot path.
+ */
+public class DDOutputFormat implements OutputFormat {
+
+ private final OutputFormat delegate;
+ private final TestEventsHandler handler;
+ private final String frameworkVersion;
+
+ // Keys used as suite/test descriptors in the handler — just the full benchmark name strings.
+ // We keep suite and test keys separate so the handler can manage their lifetimes independently.
+ private volatile String currentSuiteKey;
+ private volatile String currentTestKey;
+
+ public DDOutputFormat(OutputFormat delegate, String frameworkVersion) {
+ this.delegate = delegate;
+ this.frameworkVersion = frameworkVersion;
+ this.handler =
+ InstrumentationBridge.createTestEventsHandler(
+ JmhUtils.FRAMEWORK_NAME, null, null, Collections.emptyList());
+ }
+
+ @Override
+ public void startBenchmark(BenchmarkParams benchParams) {
+ delegate.startBenchmark(benchParams);
+
+ String fullName = benchParams.getBenchmark();
+ String[] parts = JmhUtils.splitBenchmarkName(fullName);
+ String suiteName = parts[0];
+ String testName = parts[1];
+ String testParameters = JmhUtils.testParameters(fullName);
+
+ currentSuiteKey = suiteName + "#" + fullName;
+ currentTestKey = fullName;
+
+ handler.onTestSuiteStart(
+ currentSuiteKey,
+ suiteName,
+ JmhUtils.FRAMEWORK_NAME,
+ frameworkVersion,
+ null,
+ Collections.emptyList(),
+ false,
+ TestFrameworkInstrumentation.JMH,
+ null);
+
+ handler.onTestStart(
+ currentSuiteKey,
+ currentTestKey,
+ testName,
+ JmhUtils.FRAMEWORK_NAME,
+ frameworkVersion,
+ testParameters,
+ Collections.emptyList(),
+ TestSourceData.UNKNOWN,
+ null,
+ null);
+ }
+
+ @Override
+ public void endBenchmark(BenchmarkResult result) {
+ String suiteKey = currentSuiteKey;
+ String testKey = currentTestKey;
+ if (suiteKey == null || testKey == null) {
+ delegate.endBenchmark(result);
+ return;
+ }
+
+ try {
+ tagBenchmarkMetrics(result);
+ handler.onTestFinish(testKey, null, null);
+ handler.onTestSuiteFinish(suiteKey, null);
+ } finally {
+ delegate.endBenchmark(result);
+ }
+ }
+
+ private void tagBenchmarkMetrics(BenchmarkResult result) {
+ AgentSpan span = AgentTracer.activeSpan();
+ if (span == null) {
+ return;
+ }
+
+ BenchmarkParams params = result.getParams();
+ span.setTag(BENCHMARK_MODE, params.getMode().shortLabel());
+ span.setTag(BENCHMARK_ITERATIONS, params.getMeasurement().getCount());
+ span.setTag(BENCHMARK_WARMUP_ITERATIONS, params.getWarmup().getCount());
+ span.setTag(BENCHMARK_FORKS, params.getForks());
+ span.setTag(BENCHMARK_THREADS, params.getThreads());
+ span.setTag(BENCHMARK_TIME_UNIT, params.getTimeUnit().name());
+
+ Result> primary = result.getPrimaryResult();
+ span.setMetric(BENCHMARK_VALUE, primary.getScore());
+ span.setTag(BENCHMARK_UNIT, primary.getScoreUnit());
+
+ double error = primary.getScoreError();
+ if (!Double.isNaN(error)) {
+ span.setMetric(BENCHMARK_ERROR, error);
+ }
+
+ Statistics stats = primary.getStatistics();
+ if (stats.getN() > 1) {
+ span.setMetric(BENCHMARK_P50, stats.getPercentile(50));
+ span.setMetric(BENCHMARK_P90, stats.getPercentile(90));
+ span.setMetric(BENCHMARK_P95, stats.getPercentile(95));
+ span.setMetric(BENCHMARK_P99, stats.getPercentile(99));
+ span.setMetric(BENCHMARK_MIN, stats.getMin());
+ span.setMetric(BENCHMARK_MAX, stats.getMax());
+ span.setMetric(BENCHMARK_SAMPLE_COUNT, stats.getN());
+ }
+ }
+
+ // ---- Delegation-only methods ----
+
+ @Override
+ public void iteration(
+ BenchmarkParams benchParams, org.openjdk.jmh.infra.IterationParams params, int iteration) {
+ delegate.iteration(benchParams, params, iteration);
+ }
+
+ @Override
+ public void iterationResult(
+ BenchmarkParams benchParams,
+ org.openjdk.jmh.infra.IterationParams params,
+ int iteration,
+ org.openjdk.jmh.results.IterationResult data) {
+ delegate.iterationResult(benchParams, params, iteration, data);
+ }
+
+ @Override
+ public void startRun() {
+ delegate.startRun();
+ }
+
+ @Override
+ public void endRun(java.util.Collection result) {
+ try {
+ handler.close();
+ } finally {
+ delegate.endRun(result);
+ }
+ }
+
+ @Override
+ public void print(String s) {
+ delegate.print(s);
+ }
+
+ @Override
+ public void println(String s) {
+ delegate.println(s);
+ }
+
+ @Override
+ public void verbosePrintln(String s) {
+ delegate.verbosePrintln(s);
+ }
+
+ @Override
+ public void write(int b) {
+ delegate.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws java.io.IOException {
+ delegate.write(b);
+ }
+
+ @Override
+ public void flush() {
+ delegate.flush();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+}
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/main/java/datadog/trace/instrumentation/jmh/JmhInstrumentation.java b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/main/java/datadog/trace/instrumentation/jmh/JmhInstrumentation.java
new file mode 100644
index 00000000000..5bfb0013ff1
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/main/java/datadog/trace/instrumentation/jmh/JmhInstrumentation.java
@@ -0,0 +1,60 @@
+package datadog.trace.instrumentation.jmh;
+
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.agent.tooling.InstrumenterModule;
+import net.bytebuddy.asm.Advice;
+import org.openjdk.jmh.runner.format.OutputFormat;
+
+@AutoService(InstrumenterModule.class)
+public class JmhInstrumentation extends InstrumenterModule.CiVisibility
+ implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
+
+ public JmhInstrumentation() {
+ super("ci-visibility", "jmh");
+ }
+
+ @Override
+ public String instrumentedType() {
+ // Instrument BaseRunner (where the 'out' field is declared) so that the final-field write
+ // in RunnerConstructorAdvice is legal: JDK 17+ rejects writing a final field declared in a
+ // superclass from advice injected into the subclass (Runner).
+ return "org.openjdk.jmh.runner.BaseRunner";
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ packageName + ".JmhUtils", packageName + ".DDOutputFormat",
+ };
+ }
+
+ @Override
+ public void methodAdvice(MethodTransformer transformer) {
+ transformer.applyAdvice(
+ isConstructor()
+ .and(takesArgument(0, named("org.openjdk.jmh.runner.options.Options")))
+ .and(takesArgument(1, named("org.openjdk.jmh.runner.format.OutputFormat"))),
+ JmhInstrumentation.class.getName() + "$RunnerConstructorAdvice");
+ }
+
+ public static class RunnerConstructorAdvice {
+ @Advice.OnMethodExit(suppress = Throwable.class)
+ public static void onExit(
+ @Advice.FieldValue(value = "out", readOnly = false) OutputFormat out) {
+ if (out instanceof DDOutputFormat) {
+ return;
+ }
+ String version = null;
+ try {
+ version = org.openjdk.jmh.Main.class.getPackage().getImplementationVersion();
+ } catch (Throwable ignored) {
+ }
+ out = new DDOutputFormat(out, version);
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/main/java/datadog/trace/instrumentation/jmh/JmhUtils.java b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/main/java/datadog/trace/instrumentation/jmh/JmhUtils.java
new file mode 100644
index 00000000000..b5bfea0e2ef
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/main/java/datadog/trace/instrumentation/jmh/JmhUtils.java
@@ -0,0 +1,52 @@
+package datadog.trace.instrumentation.jmh;
+
+import javax.annotation.Nullable;
+
+public final class JmhUtils {
+
+ static final String FRAMEWORK_NAME = "jmh";
+
+ /**
+ * Splits a JMH benchmark name into suite (class) and method parts.
+ *
+ * JMH names have the form {@code "com.example.MyBenchmark.myMethod"} or, when {@code @Param}
+ * combinations are present, {@code "com.example.MyBenchmark.myMethod:size=1000,threads=4"}.
+ */
+ public static String[] splitBenchmarkName(String fullName) {
+ // Strip any @Param suffix before splitting on the class/method boundary
+ int colonIdx = fullName.indexOf(':');
+ String baseName = colonIdx >= 0 ? fullName.substring(0, colonIdx) : fullName;
+
+ int lastDot = baseName.lastIndexOf('.');
+ if (lastDot < 0) {
+ return new String[] {"", fullName};
+ }
+ return new String[] {baseName.substring(0, lastDot), baseName.substring(lastDot + 1)};
+ }
+
+ /**
+ * Returns the {@code test.parameters} JSON string for a parameterized benchmark, or {@code null}
+ * for an unparameterized one.
+ *
+ *
Follows the same convention as JUnit 5 parameterized tests: {@code
+ * {"metadata":{"test_name":""}}}.
+ */
+ @Nullable
+ public static String testParameters(String fullName) {
+ int colonIdx = fullName.indexOf(':');
+ if (colonIdx < 0) {
+ return null;
+ }
+ // fullName after last dot includes the param suffix, e.g. "myMethod:size=1000"
+ int lastDot = fullName.lastIndexOf('.', colonIdx);
+ String displayName = lastDot >= 0 ? fullName.substring(lastDot + 1) : fullName;
+ return "{\"metadata\":{\"test_name\":\"" + escapeJson(displayName) + "\"}}";
+ }
+
+ /** Minimal JSON string escaping for benchmark names (no unicode escaping needed). */
+ private static String escapeJson(String s) {
+ return s.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ private JmhUtils() {}
+}
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/groovy/JmhInstrumentationTest.groovy b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/groovy/JmhInstrumentationTest.groovy
new file mode 100644
index 00000000000..4b4e6a7ee5f
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/groovy/JmhInstrumentationTest.groovy
@@ -0,0 +1,51 @@
+import datadog.trace.api.DisableTestTrace
+import datadog.trace.civisibility.CiVisibilityInstrumentationTest
+import datadog.trace.instrumentation.jmh.benchmarks.SimpleBenchmark
+import datadog.trace.instrumentation.jmh.benchmarks.ParameterizedBenchmark
+import org.openjdk.jmh.runner.Runner
+import org.openjdk.jmh.runner.options.OptionsBuilder
+
+@DisableTestTrace(reason = "avoid self-tracing")
+class JmhInstrumentationTest extends CiVisibilityInstrumentationTest {
+
+ // Benchmark numeric metrics vary each run — they are verified structurally in the smoke test
+ static final List BENCHMARK_METRIC_TAGS = [
+ "content.metrics.['benchmark.value']",
+ "content.metrics.['benchmark.error']",
+ "content.metrics.['benchmark.p50']",
+ "content.metrics.['benchmark.p90']",
+ "content.metrics.['benchmark.p95']",
+ "content.metrics.['benchmark.p99']",
+ "content.metrics.['benchmark.min']",
+ "content.metrics.['benchmark.max']",
+ "content.metrics.['benchmark.sample_count']",
+ ]
+
+ def "test #testcaseName"() {
+ runBenchmark(benchmarkClass)
+ assertSpansData(testcaseName, [:], BENCHMARK_METRIC_TAGS)
+
+ where:
+ testcaseName | benchmarkClass
+ "test-benchmark-simple" | SimpleBenchmark
+ "test-benchmark-parameterized" | ParameterizedBenchmark
+ }
+
+ private void runBenchmark(Class> benchmarkClass) {
+ def options = new OptionsBuilder()
+ .include(benchmarkClass.getName())
+ .jvmArgsAppend("-Djmh.ignoreLock=true")
+ .build()
+ new Runner(options).run()
+ }
+
+ @Override
+ String instrumentedLibraryName() {
+ "jmh"
+ }
+
+ @Override
+ String instrumentedLibraryVersion() {
+ Runner.class.getPackage().getImplementationVersion()
+ }
+}
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/java/datadog/trace/instrumentation/jmh/JmhUtilsTest.java b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/java/datadog/trace/instrumentation/jmh/JmhUtilsTest.java
new file mode 100644
index 00000000000..4da75dfc901
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/java/datadog/trace/instrumentation/jmh/JmhUtilsTest.java
@@ -0,0 +1,52 @@
+package datadog.trace.instrumentation.jmh;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class JmhUtilsTest {
+
+ @Test
+ void splitBenchmarkName_simple() {
+ String[] parts = JmhUtils.splitBenchmarkName("com.example.MyBenchmark.myMethod");
+ assertArrayEquals(new String[] {"com.example.MyBenchmark", "myMethod"}, parts);
+ }
+
+ @Test
+ void splitBenchmarkName_withParams() {
+ String[] parts =
+ JmhUtils.splitBenchmarkName("com.example.MyBenchmark.myMethod:size=1000,threads=4");
+ assertArrayEquals(new String[] {"com.example.MyBenchmark", "myMethod"}, parts);
+ }
+
+ @Test
+ void splitBenchmarkName_noPackage() {
+ String[] parts = JmhUtils.splitBenchmarkName("MyBenchmark.myMethod");
+ assertArrayEquals(new String[] {"MyBenchmark", "myMethod"}, parts);
+ }
+
+ @Test
+ void splitBenchmarkName_noDot() {
+ String[] parts = JmhUtils.splitBenchmarkName("noDot");
+ assertArrayEquals(new String[] {"", "noDot"}, parts);
+ }
+
+ @Test
+ void testParameters_noParams() {
+ assertNull(JmhUtils.testParameters("com.example.MyBenchmark.myMethod"));
+ }
+
+ @Test
+ void testParameters_withParams() {
+ String result = JmhUtils.testParameters("com.example.MyBenchmark.myMethod:size=1000,threads=4");
+ assertEquals("{\"metadata\":{\"test_name\":\"myMethod:size=1000,threads=4\"}}", result);
+ }
+
+ @Test
+ void testParameters_escapesQuotes() {
+ String result = JmhUtils.testParameters("com.example.MyBenchmark.myMethod:key=\"value\"");
+ assertEquals("{\"metadata\":{\"test_name\":\"myMethod:key=\\\"value\\\"\"}}", result);
+ }
+}
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/java/datadog/trace/instrumentation/jmh/benchmarks/ParameterizedBenchmark.java b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/java/datadog/trace/instrumentation/jmh/benchmarks/ParameterizedBenchmark.java
new file mode 100644
index 00000000000..2a752e58cab
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/java/datadog/trace/instrumentation/jmh/benchmarks/ParameterizedBenchmark.java
@@ -0,0 +1,30 @@
+package datadog.trace.instrumentation.jmh.benchmarks;
+
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+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.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.MILLISECONDS)
+@Fork(0)
+@State(Scope.Benchmark)
+public class ParameterizedBenchmark {
+
+ @Param({"1", "2"})
+ int size;
+
+ @Benchmark
+ public int measure() {
+ return size * 2;
+ }
+}
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/java/datadog/trace/instrumentation/jmh/benchmarks/SimpleBenchmark.java b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/java/datadog/trace/instrumentation/jmh/benchmarks/SimpleBenchmark.java
new file mode 100644
index 00000000000..2b2b5b6ef8a
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/java/datadog/trace/instrumentation/jmh/benchmarks/SimpleBenchmark.java
@@ -0,0 +1,22 @@
+package datadog.trace.instrumentation.jmh.benchmarks;
+
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Warmup;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.MILLISECONDS)
+@Fork(0)
+public class SimpleBenchmark {
+ @Benchmark
+ public int measure() {
+ return 42;
+ }
+}
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-parameterized/coverages.ftl b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-parameterized/coverages.ftl
new file mode 100644
index 00000000000..8878e547a79
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-parameterized/coverages.ftl
@@ -0,0 +1 @@
+[ ]
\ No newline at end of file
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-parameterized/events.ftl b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-parameterized/events.ftl
new file mode 100644
index 00000000000..39dd9782269
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-parameterized/events.ftl
@@ -0,0 +1,225 @@
+[ {
+ "content" : {
+ "duration" : ${content_duration},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid},
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "library_version" : ${content_meta_library_version},
+ "span.kind" : "test_suite_end",
+ "test.framework" : "jmh",
+ "test.module" : "jmh-1.0",
+ "test.status" : "pass",
+ "test.suite" : "datadog.trace.instrumentation.jmh.benchmarks.ParameterizedBenchmark",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}
+ },
+ "name" : "jmh.test_suite",
+ "resource" : "datadog.trace.instrumentation.jmh.benchmarks.ParameterizedBenchmark",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "start" : ${content_start},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id}
+ },
+ "type" : "test_suite_end",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_2},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_2},
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "library_version" : ${content_meta_library_version},
+ "span.kind" : "test_suite_end",
+ "test.framework" : "jmh",
+ "test.module" : "jmh-1.0",
+ "test.status" : "pass",
+ "test.suite" : "datadog.trace.instrumentation.jmh.benchmarks.ParameterizedBenchmark",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2}
+ },
+ "name" : "jmh.test_suite",
+ "resource" : "datadog.trace.instrumentation.jmh.benchmarks.ParameterizedBenchmark",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "start" : ${content_start_2},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id_2}
+ },
+ "type" : "test_suite_end",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_3},
+ "error" : 0,
+ "meta" : {
+ "_dd.profiling.ctx" : "test",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "benchmark.run.mode" : "avgt",
+ "benchmark.run.time_unit" : "NANOSECONDS",
+ "benchmark.unit" : "ns/op",
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "span.kind" : "test",
+ "test.final_status" : "pass",
+ "test.framework" : "jmh",
+ "test.module" : "jmh-1.0",
+ "test.name" : "measure",
+ "test.status" : "pass",
+ "test.suite" : "datadog.trace.instrumentation.jmh.benchmarks.ParameterizedBenchmark",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "benchmark.run.forks" : 0,
+ "benchmark.run.iterations" : 1,
+ "benchmark.run.threads" : 1,
+ "benchmark.run.warmup_iterations" : 1,
+ "process_id" : ${content_metrics_process_id}
+ },
+ "name" : "jmh.test",
+ "parent_id" : ${content_parent_id},
+ "resource" : "datadog.trace.instrumentation.jmh.benchmarks.ParameterizedBenchmark.measure",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "span_id" : ${content_span_id},
+ "start" : ${content_start_3},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id},
+ "trace_id" : ${content_trace_id}
+ },
+ "type" : "test",
+ "version" : 2
+}, {
+ "content" : {
+ "duration" : ${content_duration_4},
+ "error" : 0,
+ "meta" : {
+ "_dd.profiling.ctx" : "test",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "benchmark.run.mode" : "avgt",
+ "benchmark.run.time_unit" : "NANOSECONDS",
+ "benchmark.unit" : "ns/op",
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "span.kind" : "test",
+ "test.final_status" : "pass",
+ "test.framework" : "jmh",
+ "test.module" : "jmh-1.0",
+ "test.name" : "measure",
+ "test.status" : "pass",
+ "test.suite" : "datadog.trace.instrumentation.jmh.benchmarks.ParameterizedBenchmark",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "benchmark.run.forks" : 0,
+ "benchmark.run.iterations" : 1,
+ "benchmark.run.threads" : 1,
+ "benchmark.run.warmup_iterations" : 1,
+ "process_id" : ${content_metrics_process_id}
+ },
+ "name" : "jmh.test",
+ "parent_id" : ${content_parent_id},
+ "resource" : "datadog.trace.instrumentation.jmh.benchmarks.ParameterizedBenchmark.measure",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "span_id" : ${content_span_id_2},
+ "start" : ${content_start_4},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id_2},
+ "trace_id" : ${content_trace_id_2}
+ },
+ "type" : "test",
+ "version" : 2
+}, {
+ "content" : {
+ "duration" : ${content_duration_5},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_3},
+ "_dd.profiling.ctx" : "test",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "span.kind" : "test_session_end",
+ "test.command" : "jmh-1.0",
+ "test.framework" : "jmh",
+ "test.status" : "pass",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_5},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "process_id" : ${content_metrics_process_id}
+ },
+ "name" : "jmh.test_session",
+ "resource" : "jmh-1.0",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "start" : ${content_start_5},
+ "test_session_id" : ${content_test_session_id}
+ },
+ "type" : "test_session_end",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_6},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_4},
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "library_version" : ${content_meta_library_version},
+ "span.kind" : "test_module_end",
+ "test.framework" : "jmh",
+ "test.module" : "jmh-1.0",
+ "test.status" : "pass",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_6}
+ },
+ "name" : "jmh.test_module",
+ "resource" : "jmh-1.0",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "start" : ${content_start_6},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id}
+ },
+ "type" : "test_module_end",
+ "version" : 1
+} ]
\ No newline at end of file
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-simple/coverages.ftl b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-simple/coverages.ftl
new file mode 100644
index 00000000000..8878e547a79
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-simple/coverages.ftl
@@ -0,0 +1 @@
+[ ]
\ No newline at end of file
diff --git a/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-simple/events.ftl b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-simple/events.ftl
new file mode 100644
index 00000000000..677f9ae486d
--- /dev/null
+++ b/dd-java-agent/instrumentation/jmh/jmh-1.0/src/test/resources/test-benchmark-simple/events.ftl
@@ -0,0 +1,145 @@
+[ {
+ "content" : {
+ "duration" : ${content_duration},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid},
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "library_version" : ${content_meta_library_version},
+ "span.kind" : "test_suite_end",
+ "test.framework" : "jmh",
+ "test.module" : "jmh-1.0",
+ "test.status" : "pass",
+ "test.suite" : "datadog.trace.instrumentation.jmh.benchmarks.SimpleBenchmark",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}
+ },
+ "name" : "jmh.test_suite",
+ "resource" : "datadog.trace.instrumentation.jmh.benchmarks.SimpleBenchmark",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "start" : ${content_start},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id}
+ },
+ "type" : "test_suite_end",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_2},
+ "error" : 0,
+ "meta" : {
+ "_dd.profiling.ctx" : "test",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "benchmark.run.mode" : "avgt",
+ "benchmark.run.time_unit" : "NANOSECONDS",
+ "benchmark.unit" : "ns/op",
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "span.kind" : "test",
+ "test.final_status" : "pass",
+ "test.framework" : "jmh",
+ "test.module" : "jmh-1.0",
+ "test.name" : "measure",
+ "test.status" : "pass",
+ "test.suite" : "datadog.trace.instrumentation.jmh.benchmarks.SimpleBenchmark",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "benchmark.run.forks" : 0,
+ "benchmark.run.iterations" : 1,
+ "benchmark.run.threads" : 1,
+ "benchmark.run.warmup_iterations" : 1,
+ "process_id" : ${content_metrics_process_id}
+ },
+ "name" : "jmh.test",
+ "parent_id" : ${content_parent_id},
+ "resource" : "datadog.trace.instrumentation.jmh.benchmarks.SimpleBenchmark.measure",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "span_id" : ${content_span_id},
+ "start" : ${content_start_2},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id},
+ "test_suite_id" : ${content_test_suite_id},
+ "trace_id" : ${content_trace_id}
+ },
+ "type" : "test",
+ "version" : 2
+}, {
+ "content" : {
+ "duration" : ${content_duration_3},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_2},
+ "_dd.profiling.ctx" : "test",
+ "_dd.tracer_host" : ${content_meta__dd_tracer_host},
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "language" : "jvm",
+ "library_version" : ${content_meta_library_version},
+ "runtime-id" : ${content_meta_runtime_id},
+ "span.kind" : "test_session_end",
+ "test.command" : "jmh-1.0",
+ "test.framework" : "jmh",
+ "test.status" : "pass",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3},
+ "_dd.profiling.enabled" : 0,
+ "_dd.trace_span_attribute_schema" : 0,
+ "process_id" : ${content_metrics_process_id}
+ },
+ "name" : "jmh.test_session",
+ "resource" : "jmh-1.0",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "start" : ${content_start_3},
+ "test_session_id" : ${content_test_session_id}
+ },
+ "type" : "test_session_end",
+ "version" : 1
+}, {
+ "content" : {
+ "duration" : ${content_duration_4},
+ "error" : 0,
+ "meta" : {
+ "_dd.p.tid" : ${content_meta__dd_p_tid_3},
+ "component" : "jmh",
+ "dummy_ci_tag" : "dummy_ci_tag_value",
+ "env" : "none",
+ "library_version" : ${content_meta_library_version},
+ "span.kind" : "test_module_end",
+ "test.framework" : "jmh",
+ "test.module" : "jmh-1.0",
+ "test.status" : "pass",
+ "test.type" : "test",
+ "test_session.name" : "session-name"
+ },
+ "metrics" : {
+ "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4}
+ },
+ "name" : "jmh.test_module",
+ "resource" : "jmh-1.0",
+ "service" : "worker.org.gradle.process.internal.worker.gradleworkermain",
+ "start" : ${content_start_4},
+ "test_module_id" : ${content_test_module_id},
+ "test_session_id" : ${content_test_session_id}
+ },
+ "type" : "test_module_end",
+ "version" : 1
+} ]
\ No newline at end of file
diff --git a/dd-smoke-tests/jmh/build.gradle b/dd-smoke-tests/jmh/build.gradle
new file mode 100644
index 00000000000..2bbe5485741
--- /dev/null
+++ b/dd-smoke-tests/jmh/build.gradle
@@ -0,0 +1,30 @@
+apply from: "$rootDir/gradle/java.gradle"
+description = 'JMH CI Visibility Smoke Tests.'
+
+dependencies {
+ implementation group: 'org.openjdk.jmh', name: 'jmh-core', version: '1.37'
+ annotationProcessor group: 'org.openjdk.jmh', name: 'jmh-generator-annprocess', version: '1.37'
+
+ testImplementation project(':dd-smoke-tests:backend-mock')
+}
+
+tasks.withType(Test).configureEach {
+ jvmArgumentProviders.add(new CommandLineArgumentProvider() {
+ @Override
+ Iterable asArguments() {
+ def jmhJar = configurations.named("runtimeClasspath")
+ .get()
+ .find { it.name.contains("jmh-core") }
+ return ["-Ddatadog.smoketest.jmh.core.jar.path=${jmhJar}"]
+ }
+ })
+ jvmArgumentProviders.add(new CommandLineArgumentProvider() {
+ @Override
+ Iterable asArguments() {
+ def annprocJar = configurations.named("annotationProcessor")
+ .get()
+ .find { it.name.contains("jmh-generator-annprocess") }
+ return ["-Ddatadog.smoketest.jmh.annproc.jar.path=${annprocJar}"]
+ }
+ })
+}
diff --git a/dd-smoke-tests/jmh/src/main/java/datadog/smoketest/SmokeTestBenchmark.java b/dd-smoke-tests/jmh/src/main/java/datadog/smoketest/SmokeTestBenchmark.java
new file mode 100644
index 00000000000..d9a22def1ff
--- /dev/null
+++ b/dd-smoke-tests/jmh/src/main/java/datadog/smoketest/SmokeTestBenchmark.java
@@ -0,0 +1,16 @@
+package datadog.smoketest;
+
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+public class SmokeTestBenchmark {
+ @Benchmark
+ public int measure() {
+ return 42;
+ }
+}
diff --git a/dd-smoke-tests/jmh/src/test/java/datadog/smoketest/JmhSmokeTest.java b/dd-smoke-tests/jmh/src/test/java/datadog/smoketest/JmhSmokeTest.java
new file mode 100644
index 00000000000..d7e15cacf0e
--- /dev/null
+++ b/dd-smoke-tests/jmh/src/test/java/datadog/smoketest/JmhSmokeTest.java
@@ -0,0 +1,138 @@
+package datadog.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import datadog.trace.api.config.CiVisibilityConfig;
+import datadog.trace.api.config.GeneralConfig;
+import datadog.trace.civisibility.CiVisibilitySmokeTest;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class JmhSmokeTest extends CiVisibilitySmokeTest {
+
+ private static final String TEST_SERVICE_NAME = "test-jmh-service";
+ private static final int PROCESS_TIMEOUT_SECS = 120;
+
+ private static final String JMH_CORE_JAR =
+ System.getProperty("datadog.smoketest.jmh.core.jar.path");
+
+ static final MockBackend mockBackend = new MockBackend();
+
+ @BeforeEach
+ void resetMockBackend() {
+ mockBackend.reset();
+ }
+
+ @AfterAll
+ static void closeMockBackend() throws Exception {
+ mockBackend.close();
+ }
+
+ @Test
+ void testBenchmarkSpansAreEmitted() throws Exception {
+ Map agentArgs = new HashMap<>();
+ agentArgs.put(CiVisibilityConfig.CIVISIBILITY_BUILD_INSTRUMENTATION_ENABLED, "false");
+ agentArgs.put(GeneralConfig.AGENTLESS_LOG_SUBMISSION_URL, mockBackend.getIntakeUrl());
+
+ int exitCode = runBenchmark(agentArgs);
+ assertEquals(0, exitCode, "JMH process should exit cleanly");
+
+ // 4 events: test_session_end, test_module_end, test_suite_end, test (benchmark method)
+ List