diff --git a/sdk/all/build.gradle.kts b/sdk/all/build.gradle.kts index 0afeef8f4c9..3a19758f313 100644 --- a/sdk/all/build.gradle.kts +++ b/sdk/all/build.gradle.kts @@ -26,4 +26,5 @@ dependencies { testImplementation(project(":sdk:testing")) jmh(project(":sdk:testing")) + jmh(project(":api:incubator")) } diff --git a/sdk/all/src/jmh/java/io/opentelemetry/sdk/LogRecordBenchmark.java b/sdk/all/src/jmh/java/io/opentelemetry/sdk/LogRecordBenchmark.java new file mode 100644 index 00000000000..ae54e627221 --- /dev/null +++ b/sdk/all/src/jmh/java/io/opentelemetry/sdk/LogRecordBenchmark.java @@ -0,0 +1,176 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder; +import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Group; +import org.openjdk.jmh.annotations.GroupThreads; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +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; + +/** + * This benchmark measures the performance of recording log records. It includes the following + * dimensions: + * + * + * + *

Each operation consists of recording {@link LogRecordBenchmark#RECORDS_PER_INVOCATION} log + * records. + * + *

In order to isolate the record path while remaining realistic, the benchmark uses a {@link + * BatchLogRecordProcessor} paired with a noop {@link LogRecordExporter}. In order to avoid quickly + * outpacing the batch processor queue and dropping log records, the processor is configured with a + * queue size of {@link LogRecordBenchmark#RECORDS_PER_INVOCATION} * {@link + * LogRecordBenchmark#MAX_THREADS} and is flushed after each invocation. + */ +public class LogRecordBenchmark { + + private static final int RECORDS_PER_INVOCATION = BenchmarkUtils.RECORDS_PER_INVOCATION; + private static final int MAX_THREADS = 4; + private static final int QUEUE_SIZE = RECORDS_PER_INVOCATION * MAX_THREADS; + + @State(Scope.Benchmark) + public static class BenchmarkState { + + // The size of the log record, dictated by the number of attributes attached to it. + @Param LogRecordSize logSize; + + SdkLoggerProvider loggerProvider; + Logger logger; + long timestampNanos = Clock.getDefault().now(); + List> attributeKeys; + List attributeValues; + String body; + @Nullable Throwable exception; + + @Setup + public void setup() { + loggerProvider = + SdkLoggerProvider.builder() + // Configure a batch processor with a noop exporter (LogRecordExporter.composite() is + // a shortcut for a noop exporter). This allows testing the throughput / performance + // impact of BatchLogRecordProcessor, which is essential for real workloads, while + // avoiding noise from LogRecordExporters whose performance is subject to + // implementation and network details. + .addLogRecordProcessor( + BatchLogRecordProcessor.builder(LogRecordExporter.composite()) + .setMaxQueueSize(QUEUE_SIZE) + .build()) + .build(); + logger = loggerProvider.get("benchmark"); + + attributeKeys = new ArrayList<>(logSize.attributes); + attributeValues = new ArrayList<>(logSize.attributes); + for (int i = 0; i < logSize.attributes; i++) { + attributeKeys.add(AttributeKey.stringKey("key" + i)); + attributeValues.add("value" + i); + } + + body = randomString(logSize.bodyChars); + + exception = logSize.hasException ? new Exception("test exception") : null; + } + + @TearDown(Level.Invocation) + public void flush() { + loggerProvider.forceFlush().join(10, TimeUnit.SECONDS); + } + + @TearDown + public void tearDown() { + loggerProvider.shutdown(); + } + } + + @Benchmark + @Group("threads1") + @GroupThreads(1) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 5, time = 1) + @OperationsPerInvocation(RECORDS_PER_INVOCATION) + public void record_SingleThread(BenchmarkState benchmarkState) { + record(benchmarkState); + } + + @Benchmark + @Group("threads" + MAX_THREADS) + @GroupThreads(MAX_THREADS) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 5, time = 1) + @OperationsPerInvocation(RECORDS_PER_INVOCATION) + public void record_MultipleThreads(BenchmarkState benchmarkState) { + record(benchmarkState); + } + + private static void record(BenchmarkState benchmarkState) { + for (int i = 0; i < RECORDS_PER_INVOCATION; i++) { + LogRecordBuilder builder = + benchmarkState + .logger + .logRecordBuilder() + .setTimestamp(benchmarkState.timestampNanos, TimeUnit.NANOSECONDS); + for (int j = 0; j < benchmarkState.attributeKeys.size(); j++) { + builder.setAttribute( + benchmarkState.attributeKeys.get(j), benchmarkState.attributeValues.get(j)); + } + if (benchmarkState.exception != null) { + ((ExtendedLogRecordBuilder) builder).setException(benchmarkState.exception); + } + builder.setBody(benchmarkState.body); + builder.emit(); + } + } + + public enum LogRecordSize { + SMALL(0, 0, /* hasException= */ false), + MEDIUM(10, 100, /* hasException= */ false), + LARGE(100, 10000, /* hasException= */ true); + + private final int attributes; + private final int bodyChars; + private final boolean hasException; + + LogRecordSize(int attributes, int bodyChars, boolean hasException) { + this.attributes = attributes; + this.bodyChars = bodyChars; + this.hasException = hasException; + } + } + + private static String randomString(int length) { + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append((char) ('a' + (int) (Math.random() * 26))); + } + return sb.toString(); + } +} diff --git a/sdk/all/src/jmh/java/io/opentelemetry/sdk/SpanRecordBenchmark.java b/sdk/all/src/jmh/java/io/opentelemetry/sdk/SpanRecordBenchmark.java index e73e5bd0e0b..69bc3198001 100644 --- a/sdk/all/src/jmh/java/io/opentelemetry/sdk/SpanRecordBenchmark.java +++ b/sdk/all/src/jmh/java/io/opentelemetry/sdk/SpanRecordBenchmark.java @@ -84,7 +84,7 @@ public void setup() { .build()) .setSampler(Sampler.alwaysOn()) .build(); - tracer = tracerProvider.get("benchmarkTracer"); + tracer = tracerProvider.get("benchmark"); attributeKeys = new ArrayList<>(spanSize.attributes); attributeValues = new ArrayList<>(spanSize.attributes);