diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt index df26146497b..71f05afd4d1 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt @@ -1,2 +1,4 @@ Comparing source compatibility of against -No changes. \ No newline at end of file +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.sdk.common.Clock (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) long now(boolean) diff --git a/sdk/all/src/test/java/io/opentelemetry/sdk/common/SystemClockTest.java b/sdk/all/src/test/java/io/opentelemetry/sdk/common/SystemClockTest.java index 5472a6a99e1..6078d3234f4 100644 --- a/sdk/all/src/test/java/io/opentelemetry/sdk/common/SystemClockTest.java +++ b/sdk/all/src/test/java/io/opentelemetry/sdk/common/SystemClockTest.java @@ -18,7 +18,7 @@ class SystemClockTest { @EnabledOnJre(JRE.JAVA_8) @Test - void millisPrecision() { + void now_millisPrecision() { // If we test many times, we can be fairly sure we didn't just get lucky with having a rounded // result on a higher than expected precision timestamp. for (int i = 0; i < 100; i++) { @@ -29,7 +29,7 @@ void millisPrecision() { @DisabledOnJre(JRE.JAVA_8) @Test - void microsPrecision() { + void now_microsPrecision() { // If we test many times, we can be fairly sure we get at least one timestamp that isn't // coincidentally rounded to millis precision. int numHasMicros = 0; @@ -41,4 +41,29 @@ void microsPrecision() { } assertThat(numHasMicros).isNotZero(); } + + @Test + void now_lowPrecision() { + // If we test many times, we can be fairly sure we didn't just get lucky with having a rounded + // result on a higher than expected precision timestamp. + for (int i = 0; i < 100; i++) { + long now = SystemClock.getInstance().now(false); + assertThat(now % 1000000).isZero(); + } + } + + @DisabledOnJre(JRE.JAVA_8) + @Test + void now_highPrecision() { + // If we test many times, we can be fairly sure we get at least one timestamp that isn't + // coincidentally rounded to millis precision. + int numHasMicros = 0; + for (int i = 0; i < 100; i++) { + long now = SystemClock.getInstance().now(true); + if (now % 1000000 != 0) { + numHasMicros++; + } + } + assertThat(numHasMicros).isNotZero(); + } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/Clock.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/Clock.java index 9c61dce3271..44a5e02eee6 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/common/Clock.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/Clock.java @@ -34,9 +34,27 @@ static Clock getDefault() { * // Spend time... * long durationNanos = clock.now() - startNanos; * } + * + *

Calling this is equivalent to calling {@link #now(boolean)} with {@code highPrecision=true}. */ long now(); + /** + * Returns the current epoch timestamp in nanos from this clock. + * + *

This overload of {@link #now()} includes a {@code highPrecision} argument which specifies + * whether the implementation should attempt to resolve higher precision at the potential expense + * of performance. For example, in java 9+ its sometimes possible to resolve ns precision higher + * than the ms precision of {@link System#currentTimeMillis()}, but doing so incurs a performance + * penalty which some callers may wish to avoid. In contrast, we don't currently know if resolving + * ns precision is possible in java 8, regardless of the value of {@code highPrecision}. + * + *

See {@link #now()} javadoc for details on usage. + */ + default long now(boolean highPrecision) { + return now(); + } + /** * Returns a time measurement with nanosecond precision that can only be used to calculate elapsed * time. diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/SystemClock.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/SystemClock.java index 23265b3b5b4..3131f6834e0 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/common/SystemClock.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/SystemClock.java @@ -6,6 +6,7 @@ package io.opentelemetry.sdk.common; import io.opentelemetry.sdk.internal.JavaVersionSpecific; +import java.util.concurrent.TimeUnit; import javax.annotation.concurrent.ThreadSafe; /** @@ -26,7 +27,15 @@ static Clock getInstance() { @Override public long now() { - return JavaVersionSpecific.get().currentTimeNanos(); + return now(true); + } + + @Override + public long now(boolean highPrecision) { + if (highPrecision) { + return JavaVersionSpecific.get().currentTimeNanos(); + } + return TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); } @Override diff --git a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/ExemplarClockBenchmarks.java b/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/ExemplarClockBenchmarks.java new file mode 100644 index 00000000000..28d0c855134 --- /dev/null +++ b/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/ExemplarClockBenchmarks.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.sdk.common.Clock; +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; + +/** + * {@code io.opentelemetry.sdk.metrics.internal.exemplar.ReservoirCell} relies on {@link Clock} to + * obtain the measurement time when storing exemplar values. This benchmark illustrates the + * performance impact of using the higher precision {@link Clock#now()} instead of {@link + * Clock#now(boolean)} with {@code highPrecision=false}. + */ +@BenchmarkMode({Mode.AverageTime}) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +@Fork(1) +public class ExemplarClockBenchmarks { + + private static final Clock clock = Clock.getDefault(); + + @SuppressWarnings("ReturnValueIgnored") + @Benchmark + public void now_lowPrecision() { + clock.now(false); + } + + @SuppressWarnings("ReturnValueIgnored") + @Benchmark + public void now_highPrecision() { + clock.now(true); + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/ReservoirCell.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/ReservoirCell.java index 1adf684531a..4fd63edcd80 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/ReservoirCell.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/ReservoirCell.java @@ -68,8 +68,8 @@ synchronized void recordDoubleMeasurement(double value, Attributes attributes, C private void offerMeasurement(Attributes attributes, Context context) { this.attributes = attributes; - // Note: It may make sense in the future to attempt to pull this from an active span. - this.recordTime = clock.now(); + // High precision time is not worth the additional performance expense it incurs for exemplars + this.recordTime = clock.now(/* highPrecision= */ false); Span current = Span.fromContext(context); if (current.getSpanContext().isValid()) { this.spanContext = current.getSpanContext();