-
-
Notifications
You must be signed in to change notification settings - Fork 478
feat(time): Add Timestamp, EpochClock and AnchoredClock (JAVA-572) #6045
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package io.sentry.time; | ||
|
|
||
| import org.jetbrains.annotations.ApiStatus; | ||
| import org.jetbrains.annotations.NotNull; | ||
|
|
||
| /** | ||
| * One wall-clock reading plus a monotonic ticker, producing timestamps that are safe to subtract | ||
| * from each other. | ||
| * | ||
| * <p>Wall clocks jump. Read the wall clock twice and the device may have changed it in between, so | ||
| * the gap between the two readings is not the time that actually passed โ it can come out too | ||
| * short, too long, or negative. That is how a child span ends up starting before its parent. | ||
| * | ||
| * <p>So for a group of timestamps that will be compared with each other โ the spans of a | ||
| * transaction, the samples of a profile chunk, the frames of a replay segment โ read the wall clock | ||
| * only once (the anchor) and build the rest by adding the time a {@link MonotonicTicker} has | ||
| * measured since, which only ever moves forward. Gaps between those timestamps are then real | ||
| * elapsed time. Spans need exactly that: the protocol sends a start and an end timestamp and no | ||
| * duration, so the server subtracts them. | ||
| */ | ||
| @ApiStatus.Internal | ||
| public final class AnchoredClock { | ||
|
|
||
| private final @NotNull MonotonicTicker ticker; | ||
| private final long epochNanos; | ||
| private final long anchorTick; | ||
|
|
||
| private AnchoredClock( | ||
| final @NotNull MonotonicTicker ticker, final long epochNanos, final long anchorTick) { | ||
| this.ticker = ticker; | ||
| this.epochNanos = epochNanos; | ||
| this.anchorTick = anchorTick; | ||
| } | ||
|
|
||
| /** Takes the anchor now: one wall-clock reading and one tick, back to back. */ | ||
| public static @NotNull AnchoredClock create( | ||
| final @NotNull EpochClock epoch, final @NotNull MonotonicTicker ticker) { | ||
| return new AnchoredClock(ticker, epoch.now().epochNanos(), ticker.tickNanos()); | ||
| } | ||
|
|
||
| /** | ||
| * The anchor itself: the one timestamp here that was read from the wall clock rather than built | ||
| * from a tick. Reads no clock and never changes. | ||
| */ | ||
| public @NotNull Timestamp startTime() { | ||
| return Timestamp.ofEpochNanos(epochNanos); | ||
| } | ||
|
|
||
| /** Now: {@link #startTime()} plus the time the ticker has measured since the anchor. */ | ||
| public @NotNull Timestamp now() { | ||
| return at(ticker.tickNanos()); | ||
| } | ||
|
|
||
| private @NotNull Timestamp at(final long tickNanos) { | ||
| return Timestamp.ofEpochNanos(epochNanos + (tickNanos - anchorTick)); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package io.sentry.time; | ||
|
|
||
| import org.jetbrains.annotations.ApiStatus; | ||
| import org.jetbrains.annotations.NotNull; | ||
|
|
||
| /** | ||
| * The source of wall-clock {@link Timestamp}s (aka, instants). | ||
| * | ||
| * <p>Stamps a moment that will leave this process, an event, a breadcrumb, a session, and nothing | ||
| * else. This class should <b>not</b> be used to compute durations. For Durations, use {@link | ||
| * Stopwatch}, and a group of instants that will be subtracted from each other belongs to an {@link | ||
| * AnchoredClock}, which reads this once and projects the rest. | ||
| */ | ||
| @ApiStatus.Internal | ||
| public interface EpochClock { | ||
|
|
||
| /** The current instant. Serialize it; do not subtract it from another one. */ | ||
| @NotNull | ||
| Timestamp now(); | ||
| } |
25 changes: 25 additions & 0 deletions
25
sentry/src/main/java/io/sentry/time/InstantEpochNanos.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package io.sentry.time; | ||
|
|
||
| import io.sentry.DateUtils; | ||
| import java.time.Instant; | ||
| import org.jetbrains.annotations.ApiStatus; | ||
|
|
||
| /** | ||
| * Reads the epoch from {@link Instant}. | ||
| * | ||
| * <p>A class of its own so the reference to {@code java.time} is loaded only where {@link | ||
| * SystemEpochClock} decided to use it. Android's minSdk is below the API 26 that introduced {@code | ||
| * Instant}. | ||
| */ | ||
| @ApiStatus.Internal | ||
| @SuppressWarnings("NewApi") | ||
| final class InstantEpochNanos { | ||
|
|
||
| private InstantEpochNanos() {} | ||
|
|
||
| static long read() { | ||
|
runningcode marked this conversation as resolved.
|
||
| final Instant now = Instant.now(); | ||
| // No long overflow until year 2262 | ||
| return DateUtils.secondsToNanos(now.getEpochSecond()) + now.getNano(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package io.sentry.time; | ||
|
|
||
| import io.sentry.DateUtils; | ||
| import io.sentry.util.Platform; | ||
| import org.jetbrains.annotations.ApiStatus; | ||
| import org.jetbrains.annotations.NotNull; | ||
|
|
||
| /** | ||
| * The {@link EpochClock} backed by the system wall clock. | ||
| * | ||
| * <p>Reads the epoch at the best precision the platform offers: {@link java.time.Instant} where it | ||
| * is sub-millisecond, {@link System#currentTimeMillis()} everywhere else. Android is always the | ||
| * latter โ {@code Instant} is millisecond-granular there whether or not the build desugars it, see | ||
| * https://github.com/getsentry/sentry-java/pull/2451. | ||
| * | ||
| * <p>A millisecond anchor loses less than it looks: an {@link AnchoredClock} adds nanosecond ticks | ||
| * to one anchor, so only the anchor is coarse. | ||
| */ | ||
| @ApiStatus.Internal | ||
| public final class SystemEpochClock implements EpochClock { | ||
|
|
||
| private static final boolean INSTANT_IS_SUB_MILLISECOND = | ||
| Platform.isJvm() && Platform.isJavaNinePlus(); | ||
|
|
||
| private static final SystemEpochClock instance = new SystemEpochClock(); | ||
|
|
||
| public static @NotNull EpochClock getInstance() { | ||
| return instance; | ||
| } | ||
|
|
||
| private SystemEpochClock() {} | ||
|
|
||
| @Override | ||
| public @NotNull Timestamp now() { | ||
| return Timestamp.ofEpochNanos( | ||
| INSTANT_IS_SUB_MILLISECOND | ||
| ? InstantEpochNanos.read() | ||
| : DateUtils.millisToNanos(System.currentTimeMillis())); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package io.sentry.time; | ||
|
|
||
| import org.jetbrains.annotations.ApiStatus; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| /** | ||
| * An instant on the wall clock, as nanoseconds since the Unix epoch. | ||
| * | ||
| * <p>Unlike a {@link MonotonicTicker} tick, a timestamp means something outside this process: it | ||
| * can be serialized, stored, and compared against a value from another machine. | ||
| * | ||
| * <p>It deliberately offers no arithmetic between instants. Subtracting two independent wall-clock | ||
| * readings gives a duration the device's clock can lengthen, shorten or make negative. Durations | ||
| * come from a {@link Stopwatch}, or from two instants an {@link AnchoredClock} projected from the | ||
| * same tick. | ||
| * | ||
| * <p>Nanoseconds since the epoch overflow a long in the year 2262. | ||
| */ | ||
| @ApiStatus.Internal | ||
| public final class Timestamp { | ||
|
|
||
| private final long epochNanos; | ||
|
|
||
| private Timestamp(final long epochNanos) { | ||
| this.epochNanos = epochNanos; | ||
| } | ||
|
|
||
| public static @NotNull Timestamp ofEpochNanos(final long epochNanos) { | ||
| return new Timestamp(epochNanos); | ||
| } | ||
|
|
||
| public long epochNanos() { | ||
| return epochNanos; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(final @Nullable Object other) { | ||
| if (this == other) { | ||
| return true; | ||
| } | ||
| if (!(other instanceof Timestamp)) { | ||
| return false; | ||
| } | ||
| return epochNanos == ((Timestamp) other).epochNanos; | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return (int) (epochNanos ^ (epochNanos >>> 32)); | ||
| } | ||
|
|
||
| @Override | ||
| public @NotNull String toString() { | ||
| return "Timestamp{epochNanos=" + epochNanos + '}'; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package io.sentry.time | ||
|
|
||
| import com.google.common.truth.Truth.assertThat | ||
| import java.util.concurrent.TimeUnit.MILLISECONDS | ||
| import java.util.concurrent.TimeUnit.SECONDS | ||
| import kotlin.test.Test | ||
|
|
||
| class AnchoredClockTest { | ||
| private val epoch = FixedEpochClock(SECONDS.toNanos(1_700_000_000)) | ||
| private val ticker = TestMonotonicTicker(SECONDS.toNanos(5_000)) | ||
| private val anchored = AnchoredClock.create(epoch, ticker) | ||
|
|
||
| @Test | ||
| fun `startTime is the wall-clock reading the anchor was taken at`() { | ||
| assertThat(anchored.startTime().epochNanos()).isEqualTo(SECONDS.toNanos(1_700_000_000)) | ||
| } | ||
|
|
||
| @Test | ||
| fun `now is the anchor plus the time measured since`() { | ||
| ticker.advance(120, MILLISECONDS) | ||
|
|
||
| assertThat(anchored.now().epochNanos()) | ||
| .isEqualTo(SECONDS.toNanos(1_700_000_000) + MILLISECONDS.toNanos(120)) | ||
| } | ||
|
|
||
| @Test | ||
| fun `a wall-clock step does not move a projected instant`() { | ||
| ticker.advance(120, MILLISECONDS) | ||
| epoch.epochNanos -= SECONDS.toNanos(30) | ||
|
|
||
| assertThat(anchored.now().epochNanos()) | ||
| .isEqualTo(SECONDS.toNanos(1_700_000_000) + MILLISECONDS.toNanos(120)) | ||
| } | ||
|
|
||
| @Test | ||
| fun `two projected instants differ by measured time, across a wall-clock step`() { | ||
| val start = anchored.now() | ||
| epoch.epochNanos += SECONDS.toNanos(30) | ||
| ticker.advance(750, MILLISECONDS) | ||
| val end = anchored.now() | ||
|
|
||
| assertThat(end.epochNanos() - start.epochNanos()).isEqualTo(MILLISECONDS.toNanos(750)) | ||
| } | ||
|
|
||
| @Test | ||
| fun `a millisecond anchor still projects nanoseconds`() { | ||
| ticker.advance(1_234, java.util.concurrent.TimeUnit.NANOSECONDS) | ||
|
|
||
| assertThat(anchored.now().epochNanos()).isEqualTo(SECONDS.toNanos(1_700_000_000) + 1_234) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package io.sentry.time | ||
|
|
||
| /** An [EpochClock] whose instant only moves when a test moves it. */ | ||
| internal class FixedEpochClock(var epochNanos: Long = 0) : EpochClock { | ||
| override fun now(): Timestamp = Timestamp.ofEpochNanos(epochNanos) | ||
| } |
18 changes: 18 additions & 0 deletions
18
sentry/src/test/java/io/sentry/time/SystemEpochClockTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package io.sentry.time | ||
|
|
||
| import com.google.common.truth.Truth.assertThat | ||
| import java.util.concurrent.TimeUnit.MILLISECONDS | ||
| import kotlin.test.Test | ||
|
|
||
| class SystemEpochClockTest { | ||
| @Test | ||
| fun `now reads the system wall clock`() { | ||
| val before = MILLISECONDS.toNanos(System.currentTimeMillis()) | ||
| val now = SystemEpochClock.getInstance().now().epochNanos() | ||
| val after = MILLISECONDS.toNanos(System.currentTimeMillis()) | ||
|
|
||
| // the bounds are millisecond-truncated, so now() may sit up to a millisecond past `after` | ||
| assertThat(now).isAtLeast(before) | ||
| assertThat(now).isAtMost(after + MILLISECONDS.toNanos(1)) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package io.sentry.time | ||
|
|
||
| import com.google.common.truth.Truth.assertThat | ||
| import kotlin.test.Test | ||
| import kotlin.test.assertNotEquals | ||
|
|
||
| class TimestampTest { | ||
| @Test | ||
| fun `keeps the epoch value it was given`() { | ||
| assertThat(Timestamp.ofEpochNanos(1_700_000_000_000_000_000).epochNanos()) | ||
| .isEqualTo(1_700_000_000_000_000_000) | ||
| } | ||
|
|
||
| @Test | ||
| fun `compares by instant, whatever produced it`() { | ||
| val anchored = AnchoredClock.create(FixedEpochClock(42), TestMonotonicTicker()) | ||
|
|
||
| assertThat(Timestamp.ofEpochNanos(42)).isEqualTo(anchored.startTime()) | ||
| assertThat(Timestamp.ofEpochNanos(42).hashCode()).isEqualTo(anchored.startTime().hashCode()) | ||
| assertNotEquals(Timestamp.ofEpochNanos(42), Timestamp.ofEpochNanos(43)) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.