From 9105cd7bd7e2af6982a0ce26206e09199584683e Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 14:47:37 +0200 Subject: [PATCH 01/12] feat(time): Add uptime and elapsed-real-time clock abstractions (JAVA-571) `ICurrentDateProvider.getCurrentTimeMillis()` has two implementations that mean different things: `CurrentDateProvider` returns wall time, while `AndroidCurrentDateProvider` returns `SystemClock.uptimeMillis()`, which is monotonic and pauses in deep sleep. Every consumer has to hand-pick the one matching whatever it compares against, a wrong pairing compiles silently, and the tests inject fakes so nothing catches it. Name the guarantee instead. UptimeClock excludes time the device spent suspended and is what ANR detection needs, since counting suspended time reports a responsive main thread as blocked. ElapsedRealtimeClock includes it and is what a rate-limit window or a cache TTL needs. A call site declaring which one it wants can no longer be handed the other. Both extend Ticker, which promises only "a nanosecond counter with an arbitrary origin" so that Deadline and Stopwatch can be written once. That minimalism is deliberate: a name promising a guarantee it does not keep is the bug being fixed here. Deadline and Stopwatch exist so callers never do arithmetic on raw ticks. A tick carries no unit and no epoch, so `now - then < ttl` spelled out at each call site is where unit mix-ups, sentinels that happen to mean "boot", and wrap-unsafe comparisons come from. Deadline.passed() gives "not populated yet" a representation outside the numeric range, hasPassed() subtracts rather than compares so it holds for any origin, and remaining() rounds up so a caller scheduling work for it never wakes to find the deadline still standing. No call site is converted and no behaviour changes. Only the elapsed-real-time clock will need an Android implementation: `SystemClock.uptimeNanos()` is API 34 against a minSdk of 21, and `System.nanoTime()` is already CLOCK_MONOTONIC on Android, so it serves as the uptime clock on both platforms. --- .../main/kotlin/io/sentry/time/TestTicker.kt | 22 +++++ sentry/api/sentry.api | 38 ++++++++ .../main/java/io/sentry/SentryOptions.java | 44 +++++++++ .../main/java/io/sentry/time/Deadline.java | 79 ++++++++++++++++ .../io/sentry/time/ElapsedRealtimeClock.java | 16 ++++ .../sentry/time/JavaElapsedRealtimeClock.java | 28 ++++++ .../java/io/sentry/time/JavaUptimeClock.java | 22 +++++ .../main/java/io/sentry/time/Stopwatch.java | 35 +++++++ .../src/main/java/io/sentry/time/Ticker.java | 20 ++++ .../main/java/io/sentry/time/UptimeClock.java | 16 ++++ .../test/java/io/sentry/time/DeadlineTest.kt | 93 +++++++++++++++++++ .../io/sentry/time/SentryOptionsClockTest.kt | 34 +++++++ .../test/java/io/sentry/time/StopwatchTest.kt | 38 ++++++++ 13 files changed, 485 insertions(+) create mode 100644 sentry-test-support/src/main/kotlin/io/sentry/time/TestTicker.kt create mode 100644 sentry/src/main/java/io/sentry/time/Deadline.java create mode 100644 sentry/src/main/java/io/sentry/time/ElapsedRealtimeClock.java create mode 100644 sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java create mode 100644 sentry/src/main/java/io/sentry/time/JavaUptimeClock.java create mode 100644 sentry/src/main/java/io/sentry/time/Stopwatch.java create mode 100644 sentry/src/main/java/io/sentry/time/Ticker.java create mode 100644 sentry/src/main/java/io/sentry/time/UptimeClock.java create mode 100644 sentry/src/test/java/io/sentry/time/DeadlineTest.kt create mode 100644 sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt create mode 100644 sentry/src/test/java/io/sentry/time/StopwatchTest.kt diff --git a/sentry-test-support/src/main/kotlin/io/sentry/time/TestTicker.kt b/sentry-test-support/src/main/kotlin/io/sentry/time/TestTicker.kt new file mode 100644 index 0000000000..1b9277be4c --- /dev/null +++ b/sentry-test-support/src/main/kotlin/io/sentry/time/TestTicker.kt @@ -0,0 +1,22 @@ +package io.sentry.time + +import java.util.concurrent.TimeUnit + +/** + * A [Ticker] that only moves when a test tells it to. + * + * Advancing by an amount *and a unit* is the point: a stubbed `thenReturn(1001)` against a + * nanosecond clock is off by a factor of a million and still compiles, whereas `advance(1001, + * MILLISECONDS)` cannot be. + * + * Implements both clock guarantees so a test can inject it wherever either is declared. Production + * code must never do this — the whole purpose of the two interfaces is that one object cannot + * honestly promise both. + */ +class TestTicker(private var nanos: Long = 0) : UptimeClock, ElapsedRealtimeClock { + override fun tickNanos(): Long = nanos + + fun advance(amount: Long, unit: TimeUnit) { + nanos += unit.toNanos(amount) + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9f44481fd9..d94998d233 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3704,6 +3704,7 @@ public class io/sentry/SentryOptions { public fun getDistributionController ()Lio/sentry/IDistributionApi; public fun getDsn ()Ljava/lang/String; public fun getEffectiveOrgId ()Ljava/lang/String; + public fun getElapsedRealtimeClock ()Lio/sentry/time/ElapsedRealtimeClock; public fun getEnvelopeDiskCache ()Lio/sentry/cache/IEnvelopeCache; public fun getEnvelopeReader ()Lio/sentry/IEnvelopeReader; public fun getEnvironment ()Ljava/lang/String; @@ -3783,6 +3784,7 @@ public class io/sentry/SentryOptions { public fun getTransactionProfiler ()Lio/sentry/ITransactionProfiler; public fun getTransportFactory ()Lio/sentry/ITransportFactory; public fun getTransportGate ()Lio/sentry/transport/ITransportGate; + public fun getUptimeClock ()Lio/sentry/time/UptimeClock; public fun getVersionDetector ()Lio/sentry/IVersionDetector; public final fun getViewHierarchyExporters ()Ljava/util/List; public fun isAttachServerName ()Z @@ -3854,6 +3856,7 @@ public class io/sentry/SentryOptions { public fun setDistribution (Lio/sentry/SentryOptions$DistributionOptions;)V public fun setDistributionController (Lio/sentry/IDistributionApi;)V public fun setDsn (Ljava/lang/String;)V + public fun setElapsedRealtimeClock (Lio/sentry/time/ElapsedRealtimeClock;)V public fun setEnableAppStartProfiling (Z)V public fun setEnableAutoSessionTracking (Z)V public fun setEnableBackpressureHandling (Z)V @@ -3953,6 +3956,7 @@ public class io/sentry/SentryOptions { public fun setTransactionProfiler (Lio/sentry/ITransactionProfiler;)V public fun setTransportFactory (Lio/sentry/ITransportFactory;)V public fun setTransportGate (Lio/sentry/transport/ITransportGate;)V + public fun setUptimeClock (Lio/sentry/time/UptimeClock;)V public fun setVersionDetector (Lio/sentry/IVersionDetector;)V public fun setViewHierarchyExporters (Ljava/util/List;)V } @@ -7597,6 +7601,40 @@ public final class io/sentry/rrweb/RRWebVideoEvent$JsonKeys { public fun ()V } +public final class io/sentry/time/Deadline { + public fun hasPassed ()Z + public static fun in (Lio/sentry/time/Ticker;JLjava/util/concurrent/TimeUnit;)Lio/sentry/time/Deadline; + public fun isAfter (Lio/sentry/time/Deadline;)Z + public static fun passed (Lio/sentry/time/Ticker;)Lio/sentry/time/Deadline; + public fun remaining (Ljava/util/concurrent/TimeUnit;)J +} + +public abstract interface class io/sentry/time/ElapsedRealtimeClock : io/sentry/time/Ticker { +} + +public final class io/sentry/time/JavaElapsedRealtimeClock : io/sentry/time/ElapsedRealtimeClock { + public static fun getInstance ()Lio/sentry/time/ElapsedRealtimeClock; + public fun tickNanos ()J +} + +public final class io/sentry/time/JavaUptimeClock : io/sentry/time/UptimeClock { + public static fun getInstance ()Lio/sentry/time/UptimeClock; + public fun tickNanos ()J +} + +public final class io/sentry/time/Stopwatch { + public fun elapsed (Ljava/util/concurrent/TimeUnit;)J + public fun elapsedNanos ()J + public static fun started (Lio/sentry/time/Ticker;)Lio/sentry/time/Stopwatch; +} + +public abstract interface class io/sentry/time/Ticker { + public abstract fun tickNanos ()J +} + +public abstract interface class io/sentry/time/UptimeClock : io/sentry/time/Ticker { +} + public final class io/sentry/transport/AsyncHttpTransport : io/sentry/transport/ITransport { public fun (Lio/sentry/SentryOptions;Lio/sentry/transport/RateLimiter;Lio/sentry/transport/ITransportGate;Lio/sentry/RequestDetails;)V public fun (Lio/sentry/transport/QueuedThreadPoolExecutor;Lio/sentry/SentryOptions;Lio/sentry/transport/RateLimiter;Lio/sentry/transport/ITransportGate;Lio/sentry/transport/HttpConnection;)V diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index d7a16d4ee2..2f8f63287a 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -21,6 +21,10 @@ import io.sentry.metrics.IMetricsBatchProcessorFactory; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryTransaction; +import io.sentry.time.ElapsedRealtimeClock; +import io.sentry.time.JavaElapsedRealtimeClock; +import io.sentry.time.JavaUptimeClock; +import io.sentry.time.UptimeClock; import io.sentry.transport.ITransport; import io.sentry.transport.ITransportGate; import io.sentry.transport.NoOpEnvelopeCache; @@ -525,6 +529,16 @@ public class SentryOptions { private final @NotNull LazyEvaluator dateProvider = new LazyEvaluator<>(() -> new SentryAutoDateProvider()); + /** Clock for measuring intervals that must exclude time the device spent in deep sleep. */ + @ApiStatus.Internal + private final @NotNull LazyEvaluator uptimeClock = + new LazyEvaluator<>(() -> JavaUptimeClock.getInstance()); + + /** Clock for measuring intervals that must include time the device spent in deep sleep. */ + @ApiStatus.Internal + private final @NotNull LazyEvaluator elapsedRealtimeClock = + new LazyEvaluator<>(() -> JavaElapsedRealtimeClock.getInstance()); + private final @NotNull List performanceCollectors = new ArrayList<>(); /** Performance collector that collect performance stats while transactions run. */ @@ -3059,6 +3073,36 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) { this.dateProvider.setValue(dateProvider); } + /** + * Returns the clock used to measure intervals that must exclude deep sleep, such as ANR + * thresholds. + */ + @ApiStatus.Internal + public @NotNull UptimeClock getUptimeClock() { + return uptimeClock.getValue(); + } + + /** Sets the clock used to measure intervals that must exclude deep sleep. */ + @ApiStatus.Internal + public void setUptimeClock(final @NotNull UptimeClock uptimeClock) { + this.uptimeClock.setValue(uptimeClock); + } + + /** + * Returns the clock used to measure intervals that must include deep sleep, such as rate-limit + * windows and cache expiry. + */ + @ApiStatus.Internal + public @NotNull ElapsedRealtimeClock getElapsedRealtimeClock() { + return elapsedRealtimeClock.getValue(); + } + + /** Sets the clock used to measure intervals that must include deep sleep. */ + @ApiStatus.Internal + public void setElapsedRealtimeClock(final @NotNull ElapsedRealtimeClock elapsedRealtimeClock) { + this.elapsedRealtimeClock.setValue(elapsedRealtimeClock); + } + /** * Adds a ICollector. * diff --git a/sentry/src/main/java/io/sentry/time/Deadline.java b/sentry/src/main/java/io/sentry/time/Deadline.java new file mode 100644 index 0000000000..b3db5e6adf --- /dev/null +++ b/sentry/src/main/java/io/sentry/time/Deadline.java @@ -0,0 +1,79 @@ +package io.sentry.time; + +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A point in the future, measured on a {@link Ticker}. + * + *

Exists so that callers never do arithmetic on raw ticks. A tick carries no unit and no epoch, + * so spelling out {@code now - then < ttl} at every call site is where unit mix-ups, sentinels that + * happen to mean "boot", and wrap-unsafe {@code <} comparisons come from. Each of those is decided + * once, here. + */ +@ApiStatus.Internal +public final class Deadline { + + private final @NotNull Ticker clock; + private final long deadlineNanos; + + private Deadline(final @NotNull Ticker clock, final long deadlineNanos) { + this.clock = clock; + this.deadlineNanos = deadlineNanos; + } + + /** A deadline {@code amount} of {@code unit} from now. */ + public static @NotNull Deadline in( + final @NotNull Ticker clock, final long amount, final @NotNull TimeUnit unit) { + return new Deadline(clock, clock.tickNanos() + unit.toNanos(amount)); + } + + /** + * A deadline that has already passed. Use for state that has not been populated yet, so that + * "never set" needs no numeric sentinel and cannot be mistaken for fresh — {@code 0} is a real + * and very recent instant on any boot-relative clock. + */ + public static @NotNull Deadline passed(final @NotNull Ticker clock) { + return new Deadline(clock, clock.tickNanos()); + } + + public boolean hasPassed() { + // Subtraction rather than `<`: a tick origin is arbitrary, may be negative, and may wrap. + return clock.tickNanos() - deadlineNanos >= 0; + } + + /** + * How much time is left, rounded up, or zero once the deadline has passed. + * + *

Rounding up matters: callers schedule work for {@code remaining()} and then re-check {@link + * #hasPassed()}. Truncating would wake them a fraction early, to find the deadline still + * standing. + */ + public long remaining(final @NotNull TimeUnit unit) { + final long remainingNanos = deadlineNanos - clock.tickNanos(); + if (remainingNanos <= 0) { + return 0; + } + final long unitNanos = unit.toNanos(1); + final long whole = remainingNanos / unitNanos; + return remainingNanos % unitNanos == 0 ? whole : whole + 1; + } + + /** + * Whether this deadline falls after {@code other}. + * + * @throws IllegalArgumentException if the two were created from different clocks, whose origins + * are unrelated and whose ticks are therefore not comparable. + */ + public boolean isAfter(final @NotNull Deadline other) { + if (clock != other.clock) { + throw new IllegalArgumentException( + "Cannot compare deadlines from different clocks: " + + clock.getClass().getName() + + " and " + + other.clock.getClass().getName()); + } + return deadlineNanos - other.deadlineNanos > 0; + } +} diff --git a/sentry/src/main/java/io/sentry/time/ElapsedRealtimeClock.java b/sentry/src/main/java/io/sentry/time/ElapsedRealtimeClock.java new file mode 100644 index 0000000000..dce672e621 --- /dev/null +++ b/sentry/src/main/java/io/sentry/time/ElapsedRealtimeClock.java @@ -0,0 +1,16 @@ +package io.sentry.time; + +import org.jetbrains.annotations.ApiStatus; + +/** + * A {@link Ticker} that includes time the device spent suspended in deep sleep. + * + *

This is the clock for anything expressed in real elapsed time regardless of what the device + * was doing — a rate-limit window the server asked us to wait out, or a cache entry that should go + * stale on a wall-clock schedule. + * + *

On Android this is {@code CLOCK_BOOTTIME}, via {@code SystemClock.elapsedRealtimeNanos()}. On + * the JVM there is no comparable suspend state, so uptime and elapsed real time coincide. + */ +@ApiStatus.Internal +public interface ElapsedRealtimeClock extends Ticker {} diff --git a/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java b/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java new file mode 100644 index 0000000000..29dd1b3223 --- /dev/null +++ b/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java @@ -0,0 +1,28 @@ +package io.sentry.time; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * {@link ElapsedRealtimeClock} backed by {@link System#nanoTime()}. + * + *

A JVM has no equivalent of Android's deep sleep that it can observe, so this is the same + * source as {@link JavaUptimeClock}. The two are distinct types anyway, so that a call site + * declaring which guarantee it needs keeps documenting that intent on every platform. + */ +@ApiStatus.Internal +public final class JavaElapsedRealtimeClock implements ElapsedRealtimeClock { + + private static final JavaElapsedRealtimeClock instance = new JavaElapsedRealtimeClock(); + + public static @NotNull ElapsedRealtimeClock getInstance() { + return instance; + } + + private JavaElapsedRealtimeClock() {} + + @Override + public long tickNanos() { + return System.nanoTime(); + } +} diff --git a/sentry/src/main/java/io/sentry/time/JavaUptimeClock.java b/sentry/src/main/java/io/sentry/time/JavaUptimeClock.java new file mode 100644 index 0000000000..101eaef7d1 --- /dev/null +++ b/sentry/src/main/java/io/sentry/time/JavaUptimeClock.java @@ -0,0 +1,22 @@ +package io.sentry.time; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** {@link UptimeClock} backed by {@link System#nanoTime()}. */ +@ApiStatus.Internal +public final class JavaUptimeClock implements UptimeClock { + + private static final JavaUptimeClock instance = new JavaUptimeClock(); + + public static @NotNull UptimeClock getInstance() { + return instance; + } + + private JavaUptimeClock() {} + + @Override + public long tickNanos() { + return System.nanoTime(); + } +} diff --git a/sentry/src/main/java/io/sentry/time/Stopwatch.java b/sentry/src/main/java/io/sentry/time/Stopwatch.java new file mode 100644 index 0000000000..249a73041c --- /dev/null +++ b/sentry/src/main/java/io/sentry/time/Stopwatch.java @@ -0,0 +1,35 @@ +package io.sentry.time; + +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Measures how long something took, on a {@link Ticker}. + * + *

The counterpart to {@link Deadline}: it keeps the start tick and the unit conversion in one + * place, so call sites stop repeating {@code System.nanoTime() - startTime}. + */ +@ApiStatus.Internal +public final class Stopwatch { + + private final @NotNull Ticker clock; + private final long startNanos; + + private Stopwatch(final @NotNull Ticker clock) { + this.clock = clock; + this.startNanos = clock.tickNanos(); + } + + public static @NotNull Stopwatch started(final @NotNull Ticker clock) { + return new Stopwatch(clock); + } + + public long elapsedNanos() { + return clock.tickNanos() - startNanos; + } + + public long elapsed(final @NotNull TimeUnit unit) { + return unit.convert(elapsedNanos(), TimeUnit.NANOSECONDS); + } +} diff --git a/sentry/src/main/java/io/sentry/time/Ticker.java b/sentry/src/main/java/io/sentry/time/Ticker.java new file mode 100644 index 0000000000..a56ce0fe79 --- /dev/null +++ b/sentry/src/main/java/io/sentry/time/Ticker.java @@ -0,0 +1,20 @@ +package io.sentry.time; + +import org.jetbrains.annotations.ApiStatus; + +/** + * A monotonically increasing nanosecond counter. + * + *

This type deliberately promises very little: a tick is a number that does not go backwards, + * measured from an origin that is arbitrary and may be negative. Only differences between + * two ticks from the same instance are meaningful, and a tick must never be persisted, serialized, + * or compared against a value from another clock. + * + *

Do not implement or depend on {@code Ticker} directly. It exists so that {@link Deadline} and + * {@link Stopwatch} can be written once; callers declare {@link UptimeClock} or {@link + * ElapsedRealtimeClock}, whose names state which guarantee they provide. + */ +@ApiStatus.Internal +public interface Ticker { + long tickNanos(); +} diff --git a/sentry/src/main/java/io/sentry/time/UptimeClock.java b/sentry/src/main/java/io/sentry/time/UptimeClock.java new file mode 100644 index 0000000000..992c729e63 --- /dev/null +++ b/sentry/src/main/java/io/sentry/time/UptimeClock.java @@ -0,0 +1,16 @@ +package io.sentry.time; + +import org.jetbrains.annotations.ApiStatus; + +/** + * A {@link Ticker} that excludes time the device spent suspended in deep sleep. + * + *

This is the clock for measuring how long the CPU was actually available — most importantly ANR + * detection, where counting suspended time would report a responsive main thread as blocked. + * + *

On Android this is {@code CLOCK_MONOTONIC}, the same clock behind {@code + * SystemClock.uptimeMillis()}. On the JVM there is no comparable suspend state, so uptime and + * elapsed real time coincide. + */ +@ApiStatus.Internal +public interface UptimeClock extends Ticker {} diff --git a/sentry/src/test/java/io/sentry/time/DeadlineTest.kt b/sentry/src/test/java/io/sentry/time/DeadlineTest.kt new file mode 100644 index 0000000000..9f3e47aef2 --- /dev/null +++ b/sentry/src/test/java/io/sentry/time/DeadlineTest.kt @@ -0,0 +1,93 @@ +package io.sentry.time + +import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit.MINUTES +import java.util.concurrent.TimeUnit.SECONDS +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DeadlineTest { + @Test + fun `has not passed before the deadline`() { + val clock = TestTicker() + val deadline = Deadline.`in`(clock, 2, MINUTES) + + clock.advance(119, SECONDS) + + assertFalse(deadline.hasPassed()) + } + + @Test + fun `has passed once the deadline is reached`() { + val clock = TestTicker() + val deadline = Deadline.`in`(clock, 2, MINUTES) + + clock.advance(2, MINUTES) + + assertTrue(deadline.hasPassed()) + } + + @Test + fun `a passed deadline is never fresh, even at tick zero`() { + // Regression guard: elapsedRealtimeNanos and uptimeMillis both start at 0 on boot, so a + // numeric sentinel of 0 would read as fresh for a whole TTL after every boot. + assertTrue(Deadline.passed(TestTicker()).hasPassed()) + } + + @Test + fun `remaining counts down and floors at zero`() { + val clock = TestTicker() + val deadline = Deadline.`in`(clock, 1000, MILLISECONDS) + + assertEquals(1000, deadline.remaining(MILLISECONDS)) + + clock.advance(400, MILLISECONDS) + assertEquals(600, deadline.remaining(MILLISECONDS)) + + clock.advance(10, MINUTES) + assertEquals(0, deadline.remaining(MILLISECONDS)) + } + + @Test + fun `remaining rounds up so callers never wake before the deadline`() { + val clock = TestTicker() + val deadline = Deadline.`in`(clock, 1000, MILLISECONDS) + + // half a millisecond in: 999.5ms left, which must not report as 999 + clock.advance(500, java.util.concurrent.TimeUnit.MICROSECONDS) + + assertEquals(1000, deadline.remaining(MILLISECONDS)) + } + + @Test + fun `isAfter compares two deadlines`() { + val clock = TestTicker() + val shorter = Deadline.`in`(clock, 1, SECONDS) + val longer = Deadline.`in`(clock, 5, SECONDS) + + assertTrue(longer.isAfter(shorter)) + assertFalse(shorter.isAfter(longer)) + } + + @Test + fun `isAfter rejects deadlines from different clocks`() { + val deadline = Deadline.`in`(TestTicker(), 1, SECONDS) + val fromAnotherClock = Deadline.`in`(TestTicker(), 5, SECONDS) + + assertFailsWith { deadline.isAfter(fromAnotherClock) } + } + + @Test + fun `comparisons hold when the tick origin is negative`() { + // System.nanoTime() may start negative; only differences are meaningful. + val clock = TestTicker(Long.MIN_VALUE + 1) + val deadline = Deadline.`in`(clock, 1, SECONDS) + + assertFalse(deadline.hasPassed()) + clock.advance(1, SECONDS) + assertTrue(deadline.hasPassed()) + } +} diff --git a/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt b/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt new file mode 100644 index 0000000000..31de24d0de --- /dev/null +++ b/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt @@ -0,0 +1,34 @@ +package io.sentry.time + +import com.google.common.truth.Truth.assertThat +import io.sentry.SentryOptions +import kotlin.test.Test + +class SentryOptionsClockTest { + @Test + fun `defaults to the JVM clocks`() { + val options = SentryOptions() + + assertThat(options.uptimeClock).isInstanceOf(JavaUptimeClock::class.java) + assertThat(options.elapsedRealtimeClock).isInstanceOf(JavaElapsedRealtimeClock::class.java) + } + + @Test + fun `the default clocks are singletons`() { + assertThat(SentryOptions().uptimeClock).isSameInstanceAs(JavaUptimeClock.getInstance()) + assertThat(SentryOptions().elapsedRealtimeClock) + .isSameInstanceAs(JavaElapsedRealtimeClock.getInstance()) + } + + @Test + fun `a platform can replace either clock`() { + val options = SentryOptions() + val ticker = TestTicker() + + options.uptimeClock = ticker + options.elapsedRealtimeClock = ticker + + assertThat(options.uptimeClock).isSameInstanceAs(ticker) + assertThat(options.elapsedRealtimeClock).isSameInstanceAs(ticker) + } +} diff --git a/sentry/src/test/java/io/sentry/time/StopwatchTest.kt b/sentry/src/test/java/io/sentry/time/StopwatchTest.kt new file mode 100644 index 0000000000..bfcd924fd1 --- /dev/null +++ b/sentry/src/test/java/io/sentry/time/StopwatchTest.kt @@ -0,0 +1,38 @@ +package io.sentry.time + +import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit.NANOSECONDS +import java.util.concurrent.TimeUnit.SECONDS +import kotlin.test.Test +import kotlin.test.assertEquals + +class StopwatchTest { + @Test + fun `starts at zero`() { + assertEquals(0, Stopwatch.started(TestTicker()).elapsedNanos()) + } + + @Test + fun `reports elapsed time in the requested unit`() { + val clock = TestTicker() + val stopwatch = Stopwatch.started(clock) + + clock.advance(1500, MILLISECONDS) + + assertEquals(1, stopwatch.elapsed(SECONDS)) + assertEquals(1500, stopwatch.elapsed(MILLISECONDS)) + assertEquals(MILLISECONDS.toNanos(1500), stopwatch.elapsed(NANOSECONDS)) + } + + @Test + fun `keeps running across reads`() { + val clock = TestTicker() + val stopwatch = Stopwatch.started(clock) + + clock.advance(1, SECONDS) + assertEquals(1, stopwatch.elapsed(SECONDS)) + + clock.advance(2, SECONDS) + assertEquals(3, stopwatch.elapsed(SECONDS)) + } +} From cc297faaf7ac9b0f10601588f339b92220ffddcd Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 14:48:40 +0200 Subject: [PATCH 02/12] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d53dc1b6be..cc0a900906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Internal + +- Add internal `UptimeClock` and `ElapsedRealtimeClock` abstractions with `Deadline` and `Stopwatch` primitives ([#6028](https://github.com/getsentry/sentry-java/pull/6028)) + ## 8.55.0 ### Features From a4407ac5baa6ea4db774d54693027507225c5b18 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 15:03:29 +0200 Subject: [PATCH 03/12] ref(time): Drop the uptime clock options seam (JAVA-571) System.nanoTime() is CLOCK_MONOTONIC on Android too, and SystemClock.uptimeNanos() is API 34 against minSdk 21, so there is no platform-specific uptime implementation to install. The setter had no production caller and its only test was a test of itself, while still occupying binary-compatibility surface in sentry.api. ElapsedRealtimeClock keeps its seam: RateLimiter lives in the core module but needs SystemClock.elapsedRealtimeNanos() on Android, which only sentry-android-core can supply. UptimeClock and JavaUptimeClock remain; call sites that want the guarantee named in their type resolve the singleton directly. Co-Authored-By: Claude Opus 5 (1M context) --- sentry/api/sentry.api | 2 -- .../main/java/io/sentry/SentryOptions.java | 22 ------------------- .../java/io/sentry/time/JavaClocksTest.kt | 20 +++++++++++++++++ .../io/sentry/time/SentryOptionsClockTest.kt | 15 +++++-------- 4 files changed, 25 insertions(+), 34 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/time/JavaClocksTest.kt diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index d94998d233..deb33c50ce 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3784,7 +3784,6 @@ public class io/sentry/SentryOptions { public fun getTransactionProfiler ()Lio/sentry/ITransactionProfiler; public fun getTransportFactory ()Lio/sentry/ITransportFactory; public fun getTransportGate ()Lio/sentry/transport/ITransportGate; - public fun getUptimeClock ()Lio/sentry/time/UptimeClock; public fun getVersionDetector ()Lio/sentry/IVersionDetector; public final fun getViewHierarchyExporters ()Ljava/util/List; public fun isAttachServerName ()Z @@ -3956,7 +3955,6 @@ public class io/sentry/SentryOptions { public fun setTransactionProfiler (Lio/sentry/ITransactionProfiler;)V public fun setTransportFactory (Lio/sentry/ITransportFactory;)V public fun setTransportGate (Lio/sentry/transport/ITransportGate;)V - public fun setUptimeClock (Lio/sentry/time/UptimeClock;)V public fun setVersionDetector (Lio/sentry/IVersionDetector;)V public fun setViewHierarchyExporters (Ljava/util/List;)V } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 2f8f63287a..8e847cc6e9 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -23,8 +23,6 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.time.ElapsedRealtimeClock; import io.sentry.time.JavaElapsedRealtimeClock; -import io.sentry.time.JavaUptimeClock; -import io.sentry.time.UptimeClock; import io.sentry.transport.ITransport; import io.sentry.transport.ITransportGate; import io.sentry.transport.NoOpEnvelopeCache; @@ -529,11 +527,6 @@ public class SentryOptions { private final @NotNull LazyEvaluator dateProvider = new LazyEvaluator<>(() -> new SentryAutoDateProvider()); - /** Clock for measuring intervals that must exclude time the device spent in deep sleep. */ - @ApiStatus.Internal - private final @NotNull LazyEvaluator uptimeClock = - new LazyEvaluator<>(() -> JavaUptimeClock.getInstance()); - /** Clock for measuring intervals that must include time the device spent in deep sleep. */ @ApiStatus.Internal private final @NotNull LazyEvaluator elapsedRealtimeClock = @@ -3073,21 +3066,6 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) { this.dateProvider.setValue(dateProvider); } - /** - * Returns the clock used to measure intervals that must exclude deep sleep, such as ANR - * thresholds. - */ - @ApiStatus.Internal - public @NotNull UptimeClock getUptimeClock() { - return uptimeClock.getValue(); - } - - /** Sets the clock used to measure intervals that must exclude deep sleep. */ - @ApiStatus.Internal - public void setUptimeClock(final @NotNull UptimeClock uptimeClock) { - this.uptimeClock.setValue(uptimeClock); - } - /** * Returns the clock used to measure intervals that must include deep sleep, such as rate-limit * windows and cache expiry. diff --git a/sentry/src/test/java/io/sentry/time/JavaClocksTest.kt b/sentry/src/test/java/io/sentry/time/JavaClocksTest.kt new file mode 100644 index 0000000000..1a5a2ac893 --- /dev/null +++ b/sentry/src/test/java/io/sentry/time/JavaClocksTest.kt @@ -0,0 +1,20 @@ +package io.sentry.time + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test + +class JavaClocksTest { + @Test + fun `each clock is a singleton`() { + assertThat(JavaUptimeClock.getInstance()).isSameInstanceAs(JavaUptimeClock.getInstance()) + assertThat(JavaElapsedRealtimeClock.getInstance()) + .isSameInstanceAs(JavaElapsedRealtimeClock.getInstance()) + } + + @Test + fun `ticks do not go backwards`() { + for (clock in listOf(JavaUptimeClock.getInstance(), JavaElapsedRealtimeClock.getInstance())) { + assertThat(clock.tickNanos()).isAtMost(clock.tickNanos()) + } + } +} diff --git a/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt b/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt index 31de24d0de..4785f8761d 100644 --- a/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt +++ b/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt @@ -6,29 +6,24 @@ import kotlin.test.Test class SentryOptionsClockTest { @Test - fun `defaults to the JVM clocks`() { - val options = SentryOptions() - - assertThat(options.uptimeClock).isInstanceOf(JavaUptimeClock::class.java) - assertThat(options.elapsedRealtimeClock).isInstanceOf(JavaElapsedRealtimeClock::class.java) + fun `defaults to the JVM clock`() { + assertThat(SentryOptions().elapsedRealtimeClock) + .isInstanceOf(JavaElapsedRealtimeClock::class.java) } @Test - fun `the default clocks are singletons`() { - assertThat(SentryOptions().uptimeClock).isSameInstanceAs(JavaUptimeClock.getInstance()) + fun `the default clock is a singleton`() { assertThat(SentryOptions().elapsedRealtimeClock) .isSameInstanceAs(JavaElapsedRealtimeClock.getInstance()) } @Test - fun `a platform can replace either clock`() { + fun `a platform can replace the clock`() { val options = SentryOptions() val ticker = TestTicker() - options.uptimeClock = ticker options.elapsedRealtimeClock = ticker - assertThat(options.uptimeClock).isSameInstanceAs(ticker) assertThat(options.elapsedRealtimeClock).isSameInstanceAs(ticker) } } From 4a3806d2f9204523c7fd19b8f76a3071b8215825 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 15:08:23 +0200 Subject: [PATCH 04/12] ref(time): Trim comments that restate the type name (JAVA-571) The elapsed-real-time field carried a comment repeating its own type, and both the setter and JavaElapsedRealtimeClock restated what the ElapsedRealtimeClock javadoc already says at length. The setter javadoc now answers the question a reader actually has when they find a setter on an internal option: which platform installs one, and why the core module cannot construct it itself. Co-Authored-By: Claude Opus 5 (1M context) --- sentry/src/main/java/io/sentry/SentryOptions.java | 6 ++++-- .../main/java/io/sentry/time/JavaElapsedRealtimeClock.java | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 8e847cc6e9..5175f61102 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -527,7 +527,6 @@ public class SentryOptions { private final @NotNull LazyEvaluator dateProvider = new LazyEvaluator<>(() -> new SentryAutoDateProvider()); - /** Clock for measuring intervals that must include time the device spent in deep sleep. */ @ApiStatus.Internal private final @NotNull LazyEvaluator elapsedRealtimeClock = new LazyEvaluator<>(() -> JavaElapsedRealtimeClock.getInstance()); @@ -3075,7 +3074,10 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) { return elapsedRealtimeClock.getValue(); } - /** Sets the clock used to measure intervals that must include deep sleep. */ + /** + * Sets the clock. Android installs one backed by {@code SystemClock.elapsedRealtimeNanos()}, + * which this module cannot reference. + */ @ApiStatus.Internal public void setElapsedRealtimeClock(final @NotNull ElapsedRealtimeClock elapsedRealtimeClock) { this.elapsedRealtimeClock.setValue(elapsedRealtimeClock); diff --git a/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java b/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java index 29dd1b3223..a24c548920 100644 --- a/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java +++ b/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java @@ -6,9 +6,9 @@ /** * {@link ElapsedRealtimeClock} backed by {@link System#nanoTime()}. * - *

A JVM has no equivalent of Android's deep sleep that it can observe, so this is the same - * source as {@link JavaUptimeClock}. The two are distinct types anyway, so that a call site - * declaring which guarantee it needs keeps documenting that intent on every platform. + *

Identical to {@link JavaUptimeClock} — a JVM cannot observe deep sleep — but kept a distinct + * type so that a call site declaring which guarantee it needs documents that intent on every + * platform. */ @ApiStatus.Internal public final class JavaElapsedRealtimeClock implements ElapsedRealtimeClock { From 92809f0d6db29075ec902a88a5582cd4a2fe071b Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 17:02:05 +0200 Subject: [PATCH 05/12] feat(android): Back the elapsed-real-time clock with SystemClock (JAVA-571) Installs AndroidElapsedRealtimeClock in AndroidOptionsInitializer, beside the existing SentryAndroidDateProvider. Nothing reads the clock yet, so this changes no behaviour. Without it the options seam added in this PR is inert on Android: the default resolves to System.nanoTime(), which is CLOCK_MONOTONIC and stops in deep sleep, so a reviewer sees a setter with no caller and Android silently gets the guarantee the type says it does not provide. That was the flaw in the previous attempt at this abstraction, where the Android clock was built into a local and never installed. io.sentry.android.core.internal is in apiValidation.ignoredPackages, so there is no .api diff. Co-Authored-By: Claude Opus 5 (1M context) --- .../core/AndroidOptionsInitializer.java | 2 ++ .../time/AndroidElapsedRealtimeClock.java | 20 +++++++++++++++++++ .../core/AndroidOptionsInitializerTest.kt | 8 ++++++++ 3 files changed, 30 insertions(+) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index c7c590d624..3be7b67773 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -34,6 +34,7 @@ import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator; import io.sentry.android.core.internal.modules.AssetsModulesLoader; +import io.sentry.android.core.internal.time.AndroidElapsedRealtimeClock; import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; import io.sentry.android.core.internal.util.AndroidThreadChecker; @@ -126,6 +127,7 @@ static void loadDefaultAndMetadataOptions( options.setDefaultScopeType(ScopeType.CURRENT); options.setOpenTelemetryMode(SentryOpenTelemetryMode.OFF); options.setDateProvider(new SentryAndroidDateProvider()); + options.setElapsedRealtimeClock(new AndroidElapsedRealtimeClock()); options.getLogs().setLoggerBatchProcessorFactory(new AndroidLoggerBatchProcessorFactory()); options.getMetrics().setMetricsBatchProcessorFactory(new AndroidMetricsBatchProcessorFactory()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java new file mode 100644 index 0000000000..742f54408b --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java @@ -0,0 +1,20 @@ +package io.sentry.android.core.internal.time; + +import android.os.SystemClock; +import io.sentry.time.ElapsedRealtimeClock; +import org.jetbrains.annotations.ApiStatus; + +/** + * {@link ElapsedRealtimeClock} backed by {@link SystemClock#elapsedRealtimeNanos()}. + * + *

That is {@code CLOCK_BOOTTIME}, so it keeps counting while the device is suspended — unlike + * {@link System#nanoTime()}, which the core module falls back to and which stops in deep sleep. + */ +@ApiStatus.Internal +public final class AndroidElapsedRealtimeClock implements ElapsedRealtimeClock { + + @Override + public long tickNanos() { + return SystemClock.elapsedRealtimeNanos(); + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 85fba36f7d..15753dd68c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -24,6 +24,7 @@ import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator import io.sentry.android.core.internal.modules.AssetsModulesLoader +import io.sentry.android.core.internal.time.AndroidElapsedRealtimeClock import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider import io.sentry.android.core.internal.util.AndroidThreadChecker import io.sentry.android.core.performance.AppStartMetrics @@ -748,6 +749,13 @@ class AndroidOptionsInitializerTest { assertTrue { fixture.sentryOptions.envelopeDiskCache is AndroidEnvelopeCache } } + @Test + fun `AndroidElapsedRealtimeClock is set to options`() { + fixture.initSut() + + assertTrue { fixture.sentryOptions.elapsedRealtimeClock is AndroidElapsedRealtimeClock } + } + @Test fun `When Activity Frames Tracking is enabled, the Activity Frames Tracker should be unavailable`() { fixture.initSut( From c465cd5d2d4fc4812b62f10853151b8c98e06d15 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 17:08:59 +0200 Subject: [PATCH 06/12] test(time): Drop the accessor and singleton clock tests (JAVA-571) SentryOptionsClockTest asserted that a LazyEvaluator-backed getter returns what its setter was given; AndroidOptionsInitializerTest already covers the setter for real, on the one caller that uses it. JavaClocksTest asserted singleton identity and that a nanosecond counter does not run backwards. Neither can fail without the language failing first. DeadlineTest and StopwatchTest, which cover the arithmetic this package exists to centralise, are untouched. Co-Authored-By: Claude Opus 5 (1M context) --- .../java/io/sentry/time/JavaClocksTest.kt | 20 ------------- .../io/sentry/time/SentryOptionsClockTest.kt | 29 ------------------- 2 files changed, 49 deletions(-) delete mode 100644 sentry/src/test/java/io/sentry/time/JavaClocksTest.kt delete mode 100644 sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt diff --git a/sentry/src/test/java/io/sentry/time/JavaClocksTest.kt b/sentry/src/test/java/io/sentry/time/JavaClocksTest.kt deleted file mode 100644 index 1a5a2ac893..0000000000 --- a/sentry/src/test/java/io/sentry/time/JavaClocksTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.sentry.time - -import com.google.common.truth.Truth.assertThat -import kotlin.test.Test - -class JavaClocksTest { - @Test - fun `each clock is a singleton`() { - assertThat(JavaUptimeClock.getInstance()).isSameInstanceAs(JavaUptimeClock.getInstance()) - assertThat(JavaElapsedRealtimeClock.getInstance()) - .isSameInstanceAs(JavaElapsedRealtimeClock.getInstance()) - } - - @Test - fun `ticks do not go backwards`() { - for (clock in listOf(JavaUptimeClock.getInstance(), JavaElapsedRealtimeClock.getInstance())) { - assertThat(clock.tickNanos()).isAtMost(clock.tickNanos()) - } - } -} diff --git a/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt b/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt deleted file mode 100644 index 4785f8761d..0000000000 --- a/sentry/src/test/java/io/sentry/time/SentryOptionsClockTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.sentry.time - -import com.google.common.truth.Truth.assertThat -import io.sentry.SentryOptions -import kotlin.test.Test - -class SentryOptionsClockTest { - @Test - fun `defaults to the JVM clock`() { - assertThat(SentryOptions().elapsedRealtimeClock) - .isInstanceOf(JavaElapsedRealtimeClock::class.java) - } - - @Test - fun `the default clock is a singleton`() { - assertThat(SentryOptions().elapsedRealtimeClock) - .isSameInstanceAs(JavaElapsedRealtimeClock.getInstance()) - } - - @Test - fun `a platform can replace the clock`() { - val options = SentryOptions() - val ticker = TestTicker() - - options.elapsedRealtimeClock = ticker - - assertThat(options.elapsedRealtimeClock).isSameInstanceAs(ticker) - } -} From ca31341e3aa808604125cb24f249dcb220f11784 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 17:11:54 +0200 Subject: [PATCH 07/12] ref(android): Override the elapsed-real-time clock instead of setting it SentryAndroidOptions now returns AndroidElapsedRealtimeClock from an override, so SentryOptions no longer needs a setter and the core default collapses to the singleton it always returned. Three things get better. The setter was a mutation point on an option nobody should swap, and it is gone from sentry.api. Android is correct from construction rather than from the moment AndroidOptionsInitializer runs, closing the window where a reader saw System.nanoTime(). And consumers that take a clock in their constructor, as RateLimiter will, keep their own injection point for tests, so nothing lost a seam. The cost is that this is the only getter SentryAndroidOptions overrides; every other platform swap is installed in AndroidOptionsInitializer. Those are user-replaceable options, though, and this one is internal. Co-Authored-By: Claude Opus 5 (1M context) --- .../api/sentry-android-core.api | 1 + .../core/AndroidOptionsInitializer.java | 2 -- .../android/core/SentryAndroidOptions.java | 11 +++++++++++ .../core/AndroidOptionsInitializerTest.kt | 8 -------- .../android/core/SentryAndroidOptionsTest.kt | 6 ++++++ sentry/api/sentry.api | 1 - .../src/main/java/io/sentry/SentryOptions.java | 18 ++++-------------- 7 files changed, 22 insertions(+), 25 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 8d56c36514..9e08227fd4 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -413,6 +413,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun getBeforeScreenshotCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback; public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback; public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader; + public fun getElapsedRealtimeClock ()Lio/sentry/time/ElapsedRealtimeClock; public fun getFrameMetricsCollector ()Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector; public fun getNativeSdkName ()Ljava/lang/String; public fun getNdkAppHangTimeoutIntervalMillis ()J diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 3be7b67773..c7c590d624 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -34,7 +34,6 @@ import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator; import io.sentry.android.core.internal.modules.AssetsModulesLoader; -import io.sentry.android.core.internal.time.AndroidElapsedRealtimeClock; import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; import io.sentry.android.core.internal.util.AndroidThreadChecker; @@ -127,7 +126,6 @@ static void loadDefaultAndMetadataOptions( options.setDefaultScopeType(ScopeType.CURRENT); options.setOpenTelemetryMode(SentryOpenTelemetryMode.OFF); options.setDateProvider(new SentryAndroidDateProvider()); - options.setElapsedRealtimeClock(new AndroidElapsedRealtimeClock()); options.getLogs().setLoggerBatchProcessorFactory(new AndroidLoggerBatchProcessorFactory()); options.getMetrics().setMetricsBatchProcessorFactory(new AndroidMetricsBatchProcessorFactory()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 66a3700d38..30acace2cf 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -12,11 +12,13 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.SpanStatus; +import io.sentry.android.core.internal.time.AndroidElapsedRealtimeClock; import io.sentry.android.core.internal.util.RootChecker; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.Mechanism; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; +import io.sentry.time.ElapsedRealtimeClock; import io.sentry.util.SampleRateUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -285,6 +287,9 @@ public interface BeforeCaptureCallback { private boolean enableAnrFingerprinting = true; + private final @NotNull ElapsedRealtimeClock elapsedRealtimeClock = + new AndroidElapsedRealtimeClock(); + public SentryAndroidOptions() { setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(createSdkVersion()); @@ -889,6 +894,12 @@ public void setEnableAnrFingerprinting(final boolean enableAnrFingerprinting) { this.enableAnrFingerprinting = enableAnrFingerprinting; } + @Override + @ApiStatus.Internal + public @NotNull ElapsedRealtimeClock getElapsedRealtimeClock() { + return elapsedRealtimeClock; + } + static class AndroidUserFeedbackFormHandler implements SentryFeedbackOptions.IFormHandler { @Override public void showForm( diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 15753dd68c..85fba36f7d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -24,7 +24,6 @@ import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator import io.sentry.android.core.internal.modules.AssetsModulesLoader -import io.sentry.android.core.internal.time.AndroidElapsedRealtimeClock import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider import io.sentry.android.core.internal.util.AndroidThreadChecker import io.sentry.android.core.performance.AppStartMetrics @@ -749,13 +748,6 @@ class AndroidOptionsInitializerTest { assertTrue { fixture.sentryOptions.envelopeDiskCache is AndroidEnvelopeCache } } - @Test - fun `AndroidElapsedRealtimeClock is set to options`() { - fixture.initSut() - - assertTrue { fixture.sentryOptions.elapsedRealtimeClock is AndroidElapsedRealtimeClock } - } - @Test fun `When Activity Frames Tracking is enabled, the Activity Frames Tracker should be unavailable`() { fixture.initSut( diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index 94857b9105..40c17ea6e5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -2,6 +2,7 @@ package io.sentry.android.core import io.sentry.ITransactionProfiler import io.sentry.NoOpTransactionProfiler +import io.sentry.android.core.internal.time.AndroidElapsedRealtimeClock import io.sentry.protocol.DebugImage import kotlin.test.Test import kotlin.test.assertEquals @@ -12,6 +13,11 @@ import kotlin.test.assertTrue import org.mockito.kotlin.mock class SentryAndroidOptionsTest { + @Test + fun `elapsed real-time clock counts through deep sleep`() { + assertTrue(SentryAndroidOptions().elapsedRealtimeClock is AndroidElapsedRealtimeClock) + } + @Test fun `init should set clientName`() { val sentryOptions = SentryAndroidOptions() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index deb33c50ce..9081d56307 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3855,7 +3855,6 @@ public class io/sentry/SentryOptions { public fun setDistribution (Lio/sentry/SentryOptions$DistributionOptions;)V public fun setDistributionController (Lio/sentry/IDistributionApi;)V public fun setDsn (Ljava/lang/String;)V - public fun setElapsedRealtimeClock (Lio/sentry/time/ElapsedRealtimeClock;)V public fun setEnableAppStartProfiling (Z)V public fun setEnableAutoSessionTracking (Z)V public fun setEnableBackpressureHandling (Z)V diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 5175f61102..54f19ba93d 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -527,10 +527,6 @@ public class SentryOptions { private final @NotNull LazyEvaluator dateProvider = new LazyEvaluator<>(() -> new SentryAutoDateProvider()); - @ApiStatus.Internal - private final @NotNull LazyEvaluator elapsedRealtimeClock = - new LazyEvaluator<>(() -> JavaElapsedRealtimeClock.getInstance()); - private final @NotNull List performanceCollectors = new ArrayList<>(); /** Performance collector that collect performance stats while transactions run. */ @@ -3068,19 +3064,13 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) { /** * Returns the clock used to measure intervals that must include deep sleep, such as rate-limit * windows and cache expiry. + * + *

Android overrides this with a {@code SystemClock.elapsedRealtimeNanos()}-backed clock, which + * this module cannot reference. On the JVM there is no suspend state to account for. */ @ApiStatus.Internal public @NotNull ElapsedRealtimeClock getElapsedRealtimeClock() { - return elapsedRealtimeClock.getValue(); - } - - /** - * Sets the clock. Android installs one backed by {@code SystemClock.elapsedRealtimeNanos()}, - * which this module cannot reference. - */ - @ApiStatus.Internal - public void setElapsedRealtimeClock(final @NotNull ElapsedRealtimeClock elapsedRealtimeClock) { - this.elapsedRealtimeClock.setValue(elapsedRealtimeClock); + return JavaElapsedRealtimeClock.getInstance(); } /** From ada037d1bb7b021556404c2a49fbede97e19d7c6 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 17:14:49 +0200 Subject: [PATCH 08/12] ref(android): Make AndroidElapsedRealtimeClock a singleton It took a public constructor to match `new SentryAndroidDateProvider()` on the line beside it in AndroidOptionsInitializer. That line is gone now that SentryAndroidOptions overrides the getter, so the odd one out was the clock rather than the neighbour. With getInstance() it matches the two JVM clocks, and the field it was stored in disappears: both overrides are now the same single line returning a singleton. Co-Authored-By: Claude Opus 5 (1M context) --- .../io/sentry/android/core/SentryAndroidOptions.java | 5 +---- .../core/internal/time/AndroidElapsedRealtimeClock.java | 9 +++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 30acace2cf..bfc8c717ed 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -287,9 +287,6 @@ public interface BeforeCaptureCallback { private boolean enableAnrFingerprinting = true; - private final @NotNull ElapsedRealtimeClock elapsedRealtimeClock = - new AndroidElapsedRealtimeClock(); - public SentryAndroidOptions() { setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(createSdkVersion()); @@ -897,7 +894,7 @@ public void setEnableAnrFingerprinting(final boolean enableAnrFingerprinting) { @Override @ApiStatus.Internal public @NotNull ElapsedRealtimeClock getElapsedRealtimeClock() { - return elapsedRealtimeClock; + return AndroidElapsedRealtimeClock.getInstance(); } static class AndroidUserFeedbackFormHandler implements SentryFeedbackOptions.IFormHandler { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java index 742f54408b..b550e3ecf2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java @@ -3,6 +3,7 @@ import android.os.SystemClock; import io.sentry.time.ElapsedRealtimeClock; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; /** * {@link ElapsedRealtimeClock} backed by {@link SystemClock#elapsedRealtimeNanos()}. @@ -13,6 +14,14 @@ @ApiStatus.Internal public final class AndroidElapsedRealtimeClock implements ElapsedRealtimeClock { + private static final AndroidElapsedRealtimeClock instance = new AndroidElapsedRealtimeClock(); + + public static @NotNull ElapsedRealtimeClock getInstance() { + return instance; + } + + private AndroidElapsedRealtimeClock() {} + @Override public long tickNanos() { return SystemClock.elapsedRealtimeNanos(); From 1c6019b44f6b6f3d6257ae649e7296ec6f9e7862 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 18:29:51 +0200 Subject: [PATCH 09/12] test(android): Drop the elapsed-real-time clock options test (JAVA-571) --- .../java/io/sentry/android/core/SentryAndroidOptionsTest.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index 40c17ea6e5..94857b9105 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -2,7 +2,6 @@ package io.sentry.android.core import io.sentry.ITransactionProfiler import io.sentry.NoOpTransactionProfiler -import io.sentry.android.core.internal.time.AndroidElapsedRealtimeClock import io.sentry.protocol.DebugImage import kotlin.test.Test import kotlin.test.assertEquals @@ -13,11 +12,6 @@ import kotlin.test.assertTrue import org.mockito.kotlin.mock class SentryAndroidOptionsTest { - @Test - fun `elapsed real-time clock counts through deep sleep`() { - assertTrue(SentryAndroidOptions().elapsedRealtimeClock is AndroidElapsedRealtimeClock) - } - @Test fun `init should set clientName`() { val sentryOptions = SentryAndroidOptions() From 2c76162ba20797d7475d9d63306af9d7b221f530 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 31 Aug 2026 18:32:32 +0200 Subject: [PATCH 10/12] ref(time): Rename Deadline.in to Deadline.after (JAVA-571) `in` is a Kotlin hard keyword, so every Kotlin call site had to spell it `Deadline.`in`(...)`. Tests in this repo are Kotlin, and Kotlin callers are expected in the Android and Kotlin integration modules, so the backticks would have spread rather than stayed in one test file. `after` is a plain identifier in both languages and pairs with the existing `hasPassed()` and `isAfter()` vocabulary. --- .../src/main/java/io/sentry/time/Deadline.java | 2 +- .../test/java/io/sentry/time/DeadlineTest.kt | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sentry/src/main/java/io/sentry/time/Deadline.java b/sentry/src/main/java/io/sentry/time/Deadline.java index b3db5e6adf..1a63dc5eb5 100644 --- a/sentry/src/main/java/io/sentry/time/Deadline.java +++ b/sentry/src/main/java/io/sentry/time/Deadline.java @@ -24,7 +24,7 @@ private Deadline(final @NotNull Ticker clock, final long deadlineNanos) { } /** A deadline {@code amount} of {@code unit} from now. */ - public static @NotNull Deadline in( + public static @NotNull Deadline after( final @NotNull Ticker clock, final long amount, final @NotNull TimeUnit unit) { return new Deadline(clock, clock.tickNanos() + unit.toNanos(amount)); } diff --git a/sentry/src/test/java/io/sentry/time/DeadlineTest.kt b/sentry/src/test/java/io/sentry/time/DeadlineTest.kt index 9f3e47aef2..acc623a796 100644 --- a/sentry/src/test/java/io/sentry/time/DeadlineTest.kt +++ b/sentry/src/test/java/io/sentry/time/DeadlineTest.kt @@ -13,7 +13,7 @@ class DeadlineTest { @Test fun `has not passed before the deadline`() { val clock = TestTicker() - val deadline = Deadline.`in`(clock, 2, MINUTES) + val deadline = Deadline.after(clock, 2, MINUTES) clock.advance(119, SECONDS) @@ -23,7 +23,7 @@ class DeadlineTest { @Test fun `has passed once the deadline is reached`() { val clock = TestTicker() - val deadline = Deadline.`in`(clock, 2, MINUTES) + val deadline = Deadline.after(clock, 2, MINUTES) clock.advance(2, MINUTES) @@ -40,7 +40,7 @@ class DeadlineTest { @Test fun `remaining counts down and floors at zero`() { val clock = TestTicker() - val deadline = Deadline.`in`(clock, 1000, MILLISECONDS) + val deadline = Deadline.after(clock, 1000, MILLISECONDS) assertEquals(1000, deadline.remaining(MILLISECONDS)) @@ -54,7 +54,7 @@ class DeadlineTest { @Test fun `remaining rounds up so callers never wake before the deadline`() { val clock = TestTicker() - val deadline = Deadline.`in`(clock, 1000, MILLISECONDS) + val deadline = Deadline.after(clock, 1000, MILLISECONDS) // half a millisecond in: 999.5ms left, which must not report as 999 clock.advance(500, java.util.concurrent.TimeUnit.MICROSECONDS) @@ -65,8 +65,8 @@ class DeadlineTest { @Test fun `isAfter compares two deadlines`() { val clock = TestTicker() - val shorter = Deadline.`in`(clock, 1, SECONDS) - val longer = Deadline.`in`(clock, 5, SECONDS) + val shorter = Deadline.after(clock, 1, SECONDS) + val longer = Deadline.after(clock, 5, SECONDS) assertTrue(longer.isAfter(shorter)) assertFalse(shorter.isAfter(longer)) @@ -74,8 +74,8 @@ class DeadlineTest { @Test fun `isAfter rejects deadlines from different clocks`() { - val deadline = Deadline.`in`(TestTicker(), 1, SECONDS) - val fromAnotherClock = Deadline.`in`(TestTicker(), 5, SECONDS) + val deadline = Deadline.after(TestTicker(), 1, SECONDS) + val fromAnotherClock = Deadline.after(TestTicker(), 5, SECONDS) assertFailsWith { deadline.isAfter(fromAnotherClock) } } @@ -84,7 +84,7 @@ class DeadlineTest { fun `comparisons hold when the tick origin is negative`() { // System.nanoTime() may start negative; only differences are meaningful. val clock = TestTicker(Long.MIN_VALUE + 1) - val deadline = Deadline.`in`(clock, 1, SECONDS) + val deadline = Deadline.after(clock, 1, SECONDS) assertFalse(deadline.hasPassed()) clock.advance(1, SECONDS) From 5c664f3a499d1e69eb0011e20cd878690c14c776 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 1 Sep 2026 08:57:22 +0200 Subject: [PATCH 11/12] ref(time): Update API dump for the Deadline.after rename (JAVA-571) The Deadline.in to Deadline.after rename did not regenerate sentry/api/sentry.api, so :sentry:apiCheck failed on CI. --- sentry/api/sentry.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9081d56307..9228c9c418 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -7599,8 +7599,8 @@ public final class io/sentry/rrweb/RRWebVideoEvent$JsonKeys { } public final class io/sentry/time/Deadline { + public static fun after (Lio/sentry/time/Ticker;JLjava/util/concurrent/TimeUnit;)Lio/sentry/time/Deadline; public fun hasPassed ()Z - public static fun in (Lio/sentry/time/Ticker;JLjava/util/concurrent/TimeUnit;)Lio/sentry/time/Deadline; public fun isAfter (Lio/sentry/time/Deadline;)Z public static fun passed (Lio/sentry/time/Ticker;)Lio/sentry/time/Deadline; public fun remaining (Ljava/util/concurrent/TimeUnit;)J From 271cf66c71a1111178fc06144246ecaf469ddcd6 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 3 Sep 2026 16:35:20 +0200 Subject: [PATCH 12/12] ref(time): Collapse the clocks into a single MonotonicClock (JAVA-571) Two interfaces existed so that a call site could name which suspend behaviour it needed, but only ANR detection wanted the one that excludes deep sleep, and neither ANR path can be fooled by a suspend: ANRWatchDog reports only once ActivityManager confirms NOT_RESPONDING, and AnrProfilingIntegration parks its thread while backgrounded and resets its baseline on wake. That leaves one guarantee worth naming. Ticker, UptimeClock and JavaUptimeClock are gone, and ElapsedRealtimeClock becomes MonotonicClock, backed by elapsedRealtimeNanos() on Android and nanoTime() on the JVM. --- CHANGELOG.md | 2 +- .../api/sentry-android-core.api | 2 +- .../android/core/SentryAndroidOptions.java | 8 +++--- ...eClock.java => AndroidMonotonicClock.java} | 12 ++++---- .../{TestTicker.kt => TestMonotonicClock.kt} | 8 ++---- sentry/api/sentry.api | 27 ++++++------------ .../main/java/io/sentry/SentryOptions.java | 12 ++++---- .../main/java/io/sentry/time/Deadline.java | 10 +++---- .../io/sentry/time/ElapsedRealtimeClock.java | 16 ----------- .../sentry/time/JavaElapsedRealtimeClock.java | 28 ------------------- .../io/sentry/time/JavaMonotonicClock.java | 22 +++++++++++++++ .../java/io/sentry/time/JavaUptimeClock.java | 22 --------------- .../time/{Ticker.java => MonotonicClock.java} | 12 ++++---- .../main/java/io/sentry/time/Stopwatch.java | 8 +++--- .../main/java/io/sentry/time/UptimeClock.java | 16 ----------- .../test/java/io/sentry/time/DeadlineTest.kt | 22 +++++++-------- .../test/java/io/sentry/time/StopwatchTest.kt | 6 ++-- 17 files changed, 80 insertions(+), 153 deletions(-) rename sentry-android-core/src/main/java/io/sentry/android/core/internal/time/{AndroidElapsedRealtimeClock.java => AndroidMonotonicClock.java} (55%) rename sentry-test-support/src/main/kotlin/io/sentry/time/{TestTicker.kt => TestMonotonicClock.kt} (53%) delete mode 100644 sentry/src/main/java/io/sentry/time/ElapsedRealtimeClock.java delete mode 100644 sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java create mode 100644 sentry/src/main/java/io/sentry/time/JavaMonotonicClock.java delete mode 100644 sentry/src/main/java/io/sentry/time/JavaUptimeClock.java rename sentry/src/main/java/io/sentry/time/{Ticker.java => MonotonicClock.java} (50%) delete mode 100644 sentry/src/main/java/io/sentry/time/UptimeClock.java diff --git a/CHANGELOG.md b/CHANGELOG.md index cc0a900906..a2bd7a26d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Internal -- Add internal `UptimeClock` and `ElapsedRealtimeClock` abstractions with `Deadline` and `Stopwatch` primitives ([#6028](https://github.com/getsentry/sentry-java/pull/6028)) +- Add an internal `MonotonicClock` abstraction with `Deadline` and `Stopwatch` primitives ([#6028](https://github.com/getsentry/sentry-java/pull/6028)) ## 8.55.0 diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 9e08227fd4..08fd917251 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -413,8 +413,8 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun getBeforeScreenshotCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback; public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback; public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader; - public fun getElapsedRealtimeClock ()Lio/sentry/time/ElapsedRealtimeClock; public fun getFrameMetricsCollector ()Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector; + public fun getMonotonicClock ()Lio/sentry/time/MonotonicClock; public fun getNativeSdkName ()Ljava/lang/String; public fun getNdkAppHangTimeoutIntervalMillis ()J public fun getNdkHandlerStrategy ()I diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index bfc8c717ed..b8b9a2e398 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -12,13 +12,13 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.SpanStatus; -import io.sentry.android.core.internal.time.AndroidElapsedRealtimeClock; +import io.sentry.android.core.internal.time.AndroidMonotonicClock; import io.sentry.android.core.internal.util.RootChecker; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.Mechanism; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; -import io.sentry.time.ElapsedRealtimeClock; +import io.sentry.time.MonotonicClock; import io.sentry.util.SampleRateUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -893,8 +893,8 @@ public void setEnableAnrFingerprinting(final boolean enableAnrFingerprinting) { @Override @ApiStatus.Internal - public @NotNull ElapsedRealtimeClock getElapsedRealtimeClock() { - return AndroidElapsedRealtimeClock.getInstance(); + public @NotNull MonotonicClock getMonotonicClock() { + return AndroidMonotonicClock.getInstance(); } static class AndroidUserFeedbackFormHandler implements SentryFeedbackOptions.IFormHandler { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidMonotonicClock.java similarity index 55% rename from sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java rename to sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidMonotonicClock.java index b550e3ecf2..f47d81b5c4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidElapsedRealtimeClock.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidMonotonicClock.java @@ -1,26 +1,26 @@ package io.sentry.android.core.internal.time; import android.os.SystemClock; -import io.sentry.time.ElapsedRealtimeClock; +import io.sentry.time.MonotonicClock; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** - * {@link ElapsedRealtimeClock} backed by {@link SystemClock#elapsedRealtimeNanos()}. + * {@link MonotonicClock} backed by {@link SystemClock#elapsedRealtimeNanos()}. * *

That is {@code CLOCK_BOOTTIME}, so it keeps counting while the device is suspended — unlike * {@link System#nanoTime()}, which the core module falls back to and which stops in deep sleep. */ @ApiStatus.Internal -public final class AndroidElapsedRealtimeClock implements ElapsedRealtimeClock { +public final class AndroidMonotonicClock implements MonotonicClock { - private static final AndroidElapsedRealtimeClock instance = new AndroidElapsedRealtimeClock(); + private static final AndroidMonotonicClock instance = new AndroidMonotonicClock(); - public static @NotNull ElapsedRealtimeClock getInstance() { + public static @NotNull MonotonicClock getInstance() { return instance; } - private AndroidElapsedRealtimeClock() {} + private AndroidMonotonicClock() {} @Override public long tickNanos() { diff --git a/sentry-test-support/src/main/kotlin/io/sentry/time/TestTicker.kt b/sentry-test-support/src/main/kotlin/io/sentry/time/TestMonotonicClock.kt similarity index 53% rename from sentry-test-support/src/main/kotlin/io/sentry/time/TestTicker.kt rename to sentry-test-support/src/main/kotlin/io/sentry/time/TestMonotonicClock.kt index 1b9277be4c..e478e7b9bd 100644 --- a/sentry-test-support/src/main/kotlin/io/sentry/time/TestTicker.kt +++ b/sentry-test-support/src/main/kotlin/io/sentry/time/TestMonotonicClock.kt @@ -3,17 +3,13 @@ package io.sentry.time import java.util.concurrent.TimeUnit /** - * A [Ticker] that only moves when a test tells it to. + * A [MonotonicClock] that only moves when a test tells it to. * * Advancing by an amount *and a unit* is the point: a stubbed `thenReturn(1001)` against a * nanosecond clock is off by a factor of a million and still compiles, whereas `advance(1001, * MILLISECONDS)` cannot be. - * - * Implements both clock guarantees so a test can inject it wherever either is declared. Production - * code must never do this — the whole purpose of the two interfaces is that one object cannot - * honestly promise both. */ -class TestTicker(private var nanos: Long = 0) : UptimeClock, ElapsedRealtimeClock { +class TestMonotonicClock(private var nanos: Long = 0) : MonotonicClock { override fun tickNanos(): Long = nanos fun advance(amount: Long, unit: TimeUnit) { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9228c9c418..6afc681752 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3704,7 +3704,6 @@ public class io/sentry/SentryOptions { public fun getDistributionController ()Lio/sentry/IDistributionApi; public fun getDsn ()Ljava/lang/String; public fun getEffectiveOrgId ()Ljava/lang/String; - public fun getElapsedRealtimeClock ()Lio/sentry/time/ElapsedRealtimeClock; public fun getEnvelopeDiskCache ()Lio/sentry/cache/IEnvelopeCache; public fun getEnvelopeReader ()Lio/sentry/IEnvelopeReader; public fun getEnvironment ()Ljava/lang/String; @@ -3741,6 +3740,7 @@ public class io/sentry/SentryOptions { public fun getMaxTraceFileSize ()J public fun getMetrics ()Lio/sentry/SentryOptions$Metrics; public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader; + public fun getMonotonicClock ()Lio/sentry/time/MonotonicClock; public fun getOnDiscard ()Lio/sentry/SentryOptions$OnDiscardCallback; public fun getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback; public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode; @@ -7599,37 +7599,26 @@ public final class io/sentry/rrweb/RRWebVideoEvent$JsonKeys { } public final class io/sentry/time/Deadline { - public static fun after (Lio/sentry/time/Ticker;JLjava/util/concurrent/TimeUnit;)Lio/sentry/time/Deadline; + public static fun after (Lio/sentry/time/MonotonicClock;JLjava/util/concurrent/TimeUnit;)Lio/sentry/time/Deadline; public fun hasPassed ()Z public fun isAfter (Lio/sentry/time/Deadline;)Z - public static fun passed (Lio/sentry/time/Ticker;)Lio/sentry/time/Deadline; + public static fun passed (Lio/sentry/time/MonotonicClock;)Lio/sentry/time/Deadline; public fun remaining (Ljava/util/concurrent/TimeUnit;)J } -public abstract interface class io/sentry/time/ElapsedRealtimeClock : io/sentry/time/Ticker { -} - -public final class io/sentry/time/JavaElapsedRealtimeClock : io/sentry/time/ElapsedRealtimeClock { - public static fun getInstance ()Lio/sentry/time/ElapsedRealtimeClock; +public final class io/sentry/time/JavaMonotonicClock : io/sentry/time/MonotonicClock { + public static fun getInstance ()Lio/sentry/time/MonotonicClock; public fun tickNanos ()J } -public final class io/sentry/time/JavaUptimeClock : io/sentry/time/UptimeClock { - public static fun getInstance ()Lio/sentry/time/UptimeClock; - public fun tickNanos ()J +public abstract interface class io/sentry/time/MonotonicClock { + public abstract fun tickNanos ()J } public final class io/sentry/time/Stopwatch { public fun elapsed (Ljava/util/concurrent/TimeUnit;)J public fun elapsedNanos ()J - public static fun started (Lio/sentry/time/Ticker;)Lio/sentry/time/Stopwatch; -} - -public abstract interface class io/sentry/time/Ticker { - public abstract fun tickNanos ()J -} - -public abstract interface class io/sentry/time/UptimeClock : io/sentry/time/Ticker { + public static fun started (Lio/sentry/time/MonotonicClock;)Lio/sentry/time/Stopwatch; } public final class io/sentry/transport/AsyncHttpTransport : io/sentry/transport/ITransport { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 54f19ba93d..eba06124f5 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -21,8 +21,8 @@ import io.sentry.metrics.IMetricsBatchProcessorFactory; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryTransaction; -import io.sentry.time.ElapsedRealtimeClock; -import io.sentry.time.JavaElapsedRealtimeClock; +import io.sentry.time.JavaMonotonicClock; +import io.sentry.time.MonotonicClock; import io.sentry.transport.ITransport; import io.sentry.transport.ITransportGate; import io.sentry.transport.NoOpEnvelopeCache; @@ -3062,15 +3062,15 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) { } /** - * Returns the clock used to measure intervals that must include deep sleep, such as rate-limit - * windows and cache expiry. + * Returns the clock used to measure elapsed time, such as rate-limit windows, cache expiry and + * ANR thresholds. * *

Android overrides this with a {@code SystemClock.elapsedRealtimeNanos()}-backed clock, which * this module cannot reference. On the JVM there is no suspend state to account for. */ @ApiStatus.Internal - public @NotNull ElapsedRealtimeClock getElapsedRealtimeClock() { - return JavaElapsedRealtimeClock.getInstance(); + public @NotNull MonotonicClock getMonotonicClock() { + return JavaMonotonicClock.getInstance(); } /** diff --git a/sentry/src/main/java/io/sentry/time/Deadline.java b/sentry/src/main/java/io/sentry/time/Deadline.java index 1a63dc5eb5..5bb12047f6 100644 --- a/sentry/src/main/java/io/sentry/time/Deadline.java +++ b/sentry/src/main/java/io/sentry/time/Deadline.java @@ -5,7 +5,7 @@ import org.jetbrains.annotations.NotNull; /** - * A point in the future, measured on a {@link Ticker}. + * A point in the future, measured on a {@link MonotonicClock}. * *

Exists so that callers never do arithmetic on raw ticks. A tick carries no unit and no epoch, * so spelling out {@code now - then < ttl} at every call site is where unit mix-ups, sentinels that @@ -15,17 +15,17 @@ @ApiStatus.Internal public final class Deadline { - private final @NotNull Ticker clock; + private final @NotNull MonotonicClock clock; private final long deadlineNanos; - private Deadline(final @NotNull Ticker clock, final long deadlineNanos) { + private Deadline(final @NotNull MonotonicClock clock, final long deadlineNanos) { this.clock = clock; this.deadlineNanos = deadlineNanos; } /** A deadline {@code amount} of {@code unit} from now. */ public static @NotNull Deadline after( - final @NotNull Ticker clock, final long amount, final @NotNull TimeUnit unit) { + final @NotNull MonotonicClock clock, final long amount, final @NotNull TimeUnit unit) { return new Deadline(clock, clock.tickNanos() + unit.toNanos(amount)); } @@ -34,7 +34,7 @@ private Deadline(final @NotNull Ticker clock, final long deadlineNanos) { * "never set" needs no numeric sentinel and cannot be mistaken for fresh — {@code 0} is a real * and very recent instant on any boot-relative clock. */ - public static @NotNull Deadline passed(final @NotNull Ticker clock) { + public static @NotNull Deadline passed(final @NotNull MonotonicClock clock) { return new Deadline(clock, clock.tickNanos()); } diff --git a/sentry/src/main/java/io/sentry/time/ElapsedRealtimeClock.java b/sentry/src/main/java/io/sentry/time/ElapsedRealtimeClock.java deleted file mode 100644 index dce672e621..0000000000 --- a/sentry/src/main/java/io/sentry/time/ElapsedRealtimeClock.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.sentry.time; - -import org.jetbrains.annotations.ApiStatus; - -/** - * A {@link Ticker} that includes time the device spent suspended in deep sleep. - * - *

This is the clock for anything expressed in real elapsed time regardless of what the device - * was doing — a rate-limit window the server asked us to wait out, or a cache entry that should go - * stale on a wall-clock schedule. - * - *

On Android this is {@code CLOCK_BOOTTIME}, via {@code SystemClock.elapsedRealtimeNanos()}. On - * the JVM there is no comparable suspend state, so uptime and elapsed real time coincide. - */ -@ApiStatus.Internal -public interface ElapsedRealtimeClock extends Ticker {} diff --git a/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java b/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java deleted file mode 100644 index a24c548920..0000000000 --- a/sentry/src/main/java/io/sentry/time/JavaElapsedRealtimeClock.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.sentry.time; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -/** - * {@link ElapsedRealtimeClock} backed by {@link System#nanoTime()}. - * - *

Identical to {@link JavaUptimeClock} — a JVM cannot observe deep sleep — but kept a distinct - * type so that a call site declaring which guarantee it needs documents that intent on every - * platform. - */ -@ApiStatus.Internal -public final class JavaElapsedRealtimeClock implements ElapsedRealtimeClock { - - private static final JavaElapsedRealtimeClock instance = new JavaElapsedRealtimeClock(); - - public static @NotNull ElapsedRealtimeClock getInstance() { - return instance; - } - - private JavaElapsedRealtimeClock() {} - - @Override - public long tickNanos() { - return System.nanoTime(); - } -} diff --git a/sentry/src/main/java/io/sentry/time/JavaMonotonicClock.java b/sentry/src/main/java/io/sentry/time/JavaMonotonicClock.java new file mode 100644 index 0000000000..3475b93096 --- /dev/null +++ b/sentry/src/main/java/io/sentry/time/JavaMonotonicClock.java @@ -0,0 +1,22 @@ +package io.sentry.time; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** {@link MonotonicClock} backed by {@link System#nanoTime()}. */ +@ApiStatus.Internal +public final class JavaMonotonicClock implements MonotonicClock { + + private static final JavaMonotonicClock instance = new JavaMonotonicClock(); + + public static @NotNull MonotonicClock getInstance() { + return instance; + } + + private JavaMonotonicClock() {} + + @Override + public long tickNanos() { + return System.nanoTime(); + } +} diff --git a/sentry/src/main/java/io/sentry/time/JavaUptimeClock.java b/sentry/src/main/java/io/sentry/time/JavaUptimeClock.java deleted file mode 100644 index 101eaef7d1..0000000000 --- a/sentry/src/main/java/io/sentry/time/JavaUptimeClock.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.sentry.time; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -/** {@link UptimeClock} backed by {@link System#nanoTime()}. */ -@ApiStatus.Internal -public final class JavaUptimeClock implements UptimeClock { - - private static final JavaUptimeClock instance = new JavaUptimeClock(); - - public static @NotNull UptimeClock getInstance() { - return instance; - } - - private JavaUptimeClock() {} - - @Override - public long tickNanos() { - return System.nanoTime(); - } -} diff --git a/sentry/src/main/java/io/sentry/time/Ticker.java b/sentry/src/main/java/io/sentry/time/MonotonicClock.java similarity index 50% rename from sentry/src/main/java/io/sentry/time/Ticker.java rename to sentry/src/main/java/io/sentry/time/MonotonicClock.java index a56ce0fe79..f2d333c1a0 100644 --- a/sentry/src/main/java/io/sentry/time/Ticker.java +++ b/sentry/src/main/java/io/sentry/time/MonotonicClock.java @@ -3,18 +3,20 @@ import org.jetbrains.annotations.ApiStatus; /** - * A monotonically increasing nanosecond counter. + * A monotonically increasing nanosecond counter, including time the device spent suspended in deep + * sleep. * *

This type deliberately promises very little: a tick is a number that does not go backwards, * measured from an origin that is arbitrary and may be negative. Only differences between * two ticks from the same instance are meaningful, and a tick must never be persisted, serialized, * or compared against a value from another clock. * - *

Do not implement or depend on {@code Ticker} directly. It exists so that {@link Deadline} and - * {@link Stopwatch} can be written once; callers declare {@link UptimeClock} or {@link - * ElapsedRealtimeClock}, whose names state which guarantee they provide. + *

On Android this is {@code CLOCK_BOOTTIME}, via {@code SystemClock.elapsedRealtimeNanos()}, so + * an interval measured across a suspend reports the real time that passed rather than only the time + * the CPU was awake. On the JVM there is no comparable suspend state, so {@link System#nanoTime()} + * is equivalent. */ @ApiStatus.Internal -public interface Ticker { +public interface MonotonicClock { long tickNanos(); } diff --git a/sentry/src/main/java/io/sentry/time/Stopwatch.java b/sentry/src/main/java/io/sentry/time/Stopwatch.java index 249a73041c..bb2e3a212d 100644 --- a/sentry/src/main/java/io/sentry/time/Stopwatch.java +++ b/sentry/src/main/java/io/sentry/time/Stopwatch.java @@ -5,7 +5,7 @@ import org.jetbrains.annotations.NotNull; /** - * Measures how long something took, on a {@link Ticker}. + * Measures how long something took, on a {@link MonotonicClock}. * *

The counterpart to {@link Deadline}: it keeps the start tick and the unit conversion in one * place, so call sites stop repeating {@code System.nanoTime() - startTime}. @@ -13,15 +13,15 @@ @ApiStatus.Internal public final class Stopwatch { - private final @NotNull Ticker clock; + private final @NotNull MonotonicClock clock; private final long startNanos; - private Stopwatch(final @NotNull Ticker clock) { + private Stopwatch(final @NotNull MonotonicClock clock) { this.clock = clock; this.startNanos = clock.tickNanos(); } - public static @NotNull Stopwatch started(final @NotNull Ticker clock) { + public static @NotNull Stopwatch started(final @NotNull MonotonicClock clock) { return new Stopwatch(clock); } diff --git a/sentry/src/main/java/io/sentry/time/UptimeClock.java b/sentry/src/main/java/io/sentry/time/UptimeClock.java deleted file mode 100644 index 992c729e63..0000000000 --- a/sentry/src/main/java/io/sentry/time/UptimeClock.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.sentry.time; - -import org.jetbrains.annotations.ApiStatus; - -/** - * A {@link Ticker} that excludes time the device spent suspended in deep sleep. - * - *

This is the clock for measuring how long the CPU was actually available — most importantly ANR - * detection, where counting suspended time would report a responsive main thread as blocked. - * - *

On Android this is {@code CLOCK_MONOTONIC}, the same clock behind {@code - * SystemClock.uptimeMillis()}. On the JVM there is no comparable suspend state, so uptime and - * elapsed real time coincide. - */ -@ApiStatus.Internal -public interface UptimeClock extends Ticker {} diff --git a/sentry/src/test/java/io/sentry/time/DeadlineTest.kt b/sentry/src/test/java/io/sentry/time/DeadlineTest.kt index acc623a796..38e55710ea 100644 --- a/sentry/src/test/java/io/sentry/time/DeadlineTest.kt +++ b/sentry/src/test/java/io/sentry/time/DeadlineTest.kt @@ -12,7 +12,7 @@ import kotlin.test.assertTrue class DeadlineTest { @Test fun `has not passed before the deadline`() { - val clock = TestTicker() + val clock = TestMonotonicClock() val deadline = Deadline.after(clock, 2, MINUTES) clock.advance(119, SECONDS) @@ -22,7 +22,7 @@ class DeadlineTest { @Test fun `has passed once the deadline is reached`() { - val clock = TestTicker() + val clock = TestMonotonicClock() val deadline = Deadline.after(clock, 2, MINUTES) clock.advance(2, MINUTES) @@ -32,14 +32,14 @@ class DeadlineTest { @Test fun `a passed deadline is never fresh, even at tick zero`() { - // Regression guard: elapsedRealtimeNanos and uptimeMillis both start at 0 on boot, so a - // numeric sentinel of 0 would read as fresh for a whole TTL after every boot. - assertTrue(Deadline.passed(TestTicker()).hasPassed()) + // Regression guard: elapsedRealtimeNanos() starts at 0 on boot, so a numeric sentinel of 0 + // would read as fresh for a whole TTL after every boot. + assertTrue(Deadline.passed(TestMonotonicClock()).hasPassed()) } @Test fun `remaining counts down and floors at zero`() { - val clock = TestTicker() + val clock = TestMonotonicClock() val deadline = Deadline.after(clock, 1000, MILLISECONDS) assertEquals(1000, deadline.remaining(MILLISECONDS)) @@ -53,7 +53,7 @@ class DeadlineTest { @Test fun `remaining rounds up so callers never wake before the deadline`() { - val clock = TestTicker() + val clock = TestMonotonicClock() val deadline = Deadline.after(clock, 1000, MILLISECONDS) // half a millisecond in: 999.5ms left, which must not report as 999 @@ -64,7 +64,7 @@ class DeadlineTest { @Test fun `isAfter compares two deadlines`() { - val clock = TestTicker() + val clock = TestMonotonicClock() val shorter = Deadline.after(clock, 1, SECONDS) val longer = Deadline.after(clock, 5, SECONDS) @@ -74,8 +74,8 @@ class DeadlineTest { @Test fun `isAfter rejects deadlines from different clocks`() { - val deadline = Deadline.after(TestTicker(), 1, SECONDS) - val fromAnotherClock = Deadline.after(TestTicker(), 5, SECONDS) + val deadline = Deadline.after(TestMonotonicClock(), 1, SECONDS) + val fromAnotherClock = Deadline.after(TestMonotonicClock(), 5, SECONDS) assertFailsWith { deadline.isAfter(fromAnotherClock) } } @@ -83,7 +83,7 @@ class DeadlineTest { @Test fun `comparisons hold when the tick origin is negative`() { // System.nanoTime() may start negative; only differences are meaningful. - val clock = TestTicker(Long.MIN_VALUE + 1) + val clock = TestMonotonicClock(Long.MIN_VALUE + 1) val deadline = Deadline.after(clock, 1, SECONDS) assertFalse(deadline.hasPassed()) diff --git a/sentry/src/test/java/io/sentry/time/StopwatchTest.kt b/sentry/src/test/java/io/sentry/time/StopwatchTest.kt index bfcd924fd1..a09cf3ad4f 100644 --- a/sentry/src/test/java/io/sentry/time/StopwatchTest.kt +++ b/sentry/src/test/java/io/sentry/time/StopwatchTest.kt @@ -9,12 +9,12 @@ import kotlin.test.assertEquals class StopwatchTest { @Test fun `starts at zero`() { - assertEquals(0, Stopwatch.started(TestTicker()).elapsedNanos()) + assertEquals(0, Stopwatch.started(TestMonotonicClock()).elapsedNanos()) } @Test fun `reports elapsed time in the requested unit`() { - val clock = TestTicker() + val clock = TestMonotonicClock() val stopwatch = Stopwatch.started(clock) clock.advance(1500, MILLISECONDS) @@ -26,7 +26,7 @@ class StopwatchTest { @Test fun `keeps running across reads`() { - val clock = TestTicker() + val clock = TestMonotonicClock() val stopwatch = Stopwatch.started(clock) clock.advance(1, SECONDS)