-
-
Notifications
You must be signed in to change notification settings - Fork 476
feat(time): Add a monotonic clock abstraction (JAVA-571) #6028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d9f5693
7f0c83e
249b093
fcc54a0
8b407d9
dfe3c9e
6056068
859443f
6eecee6
036aec4
1947fb5
ab5fba1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package io.sentry.android.core.internal.time; | ||
|
|
||
| import android.os.SystemClock; | ||
| import io.sentry.time.MonotonicClock; | ||
| import org.jetbrains.annotations.ApiStatus; | ||
| import org.jetbrains.annotations.NotNull; | ||
|
|
||
| /** | ||
| * {@link MonotonicClock} backed by {@link SystemClock#elapsedRealtimeNanos()}. | ||
| * | ||
| * <p>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 AndroidMonotonicClock implements MonotonicClock { | ||
|
|
||
| private static final AndroidMonotonicClock instance = new AndroidMonotonicClock(); | ||
|
|
||
| public static @NotNull MonotonicClock getInstance() { | ||
| return instance; | ||
| } | ||
|
|
||
| private AndroidMonotonicClock() {} | ||
|
|
||
| @Override | ||
| public long tickNanos() { | ||
| return SystemClock.elapsedRealtimeNanos(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package io.sentry.time | ||
|
|
||
| import java.util.concurrent.TimeUnit | ||
|
|
||
| /** | ||
| * 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. | ||
| */ | ||
| class TestMonotonicClock(private var nanos: Long = 0) : MonotonicClock { | ||
| override fun tickNanos(): Long = nanos | ||
|
|
||
| fun advance(amount: Long, unit: TimeUnit) { | ||
| nanos += unit.toNanos(amount) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 MonotonicClock}. | ||
| * | ||
| * <p>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 MonotonicClock clock; | ||
| private 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 MonotonicClock clock, final long amount, final @NotNull TimeUnit unit) { | ||
| return new Deadline(clock, clock.tickNanos() + unit.toNanos(amount)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The calculation Suggested FixAdd input validation to the Prompt for AI AgentDid we get this right? ๐ / ๐ to inform future reviews. |
||
| } | ||
|
|
||
| /** | ||
| * 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 MonotonicClock 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. | ||
| * | ||
| * <p>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( | ||
|
runningcode marked this conversation as resolved.
|
||
| "Cannot compare deadlines from different clocks: " | ||
| + clock.getClass().getName() | ||
| + " and " | ||
| + other.clock.getClass().getName()); | ||
| } | ||
| return deadlineNanos - other.deadlineNanos > 0; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package io.sentry.time; | ||
|
|
||
| import org.jetbrains.annotations.ApiStatus; | ||
|
|
||
| /** | ||
| * A monotonically increasing nanosecond counter, including time the device spent suspended in deep | ||
| * sleep. | ||
| * | ||
| * <p>This type deliberately promises very little: a tick is a number that does not go backwards, | ||
|
runningcode marked this conversation as resolved.
|
||
| * measured from an origin that is arbitrary and may be negative. Only <em>differences</em> 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. | ||
| * | ||
| * <p>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 MonotonicClock { | ||
| long tickNanos(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 MonotonicClock}. | ||
| * | ||
| * <p>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 MonotonicClock clock; | ||
| private final long startNanos; | ||
|
|
||
| private Stopwatch(final @NotNull MonotonicClock clock) { | ||
| this.clock = clock; | ||
| this.startNanos = clock.tickNanos(); | ||
| } | ||
|
|
||
| public static @NotNull Stopwatch started(final @NotNull MonotonicClock clock) { | ||
| return new Stopwatch(clock); | ||
| } | ||
|
|
||
|
runningcode marked this conversation as resolved.
|
||
| public long elapsedNanos() { | ||
| return clock.tickNanos() - startNanos; | ||
| } | ||
|
|
||
| public long elapsed(final @NotNull TimeUnit unit) { | ||
| return unit.convert(elapsedNanos(), TimeUnit.NANOSECONDS); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.