Skip to content

Commit aa53f2e

Browse files
runningcodeclaude
andcommitted
ref(time): Read the epoch directly instead of through SentryDateProvider (JAVA-572)
DateProviderEpochClock took its instant from options.getDateProvider(), which meant allocating a SentryDate per call to read one long out of it -- and on Android that allocation also takes a System.nanoTime() reading that an EpochClock never looks at, because a Timing measures on the elapsed-real-time clock instead. The indirection bought compatibility with a tick nothing here consumes. SystemEpochClock reads the epoch itself, picking precision the way SentryAutoDateProvider does: Instant.now() on JVM 9+, currentTimeMillis() otherwise. Android is always the latter -- Instant is millisecond-granular there whether or not the build desugars it. InstantEpochNanos is a class of its own so the java.time reference is loaded only where it is used, the same reason SentryInstantDate is kept separate. The values are identical on every platform; the dependency is what changes. setDateProvider no longer reaches the epoch clock, so tests override getEpochClock() on SentryOptions, as SentryAndroidOptions already does for getElapsedRealtimeClock(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1b7d36b commit aa53f2e

7 files changed

Lines changed: 114 additions & 94 deletions

File tree

‎sentry/api/sentry.api‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7621,6 +7621,12 @@ public final class io/sentry/time/Stopwatch {
76217621
public static fun started (Lio/sentry/time/Ticker;)Lio/sentry/time/Stopwatch;
76227622
}
76237623

7624+
public final class io/sentry/time/SystemEpochClock : io/sentry/time/EpochClock {
7625+
public fun <init> (Lio/sentry/time/ElapsedRealtimeClock;)V
7626+
public fun now ()Lio/sentry/time/Timestamp;
7627+
public fun start ()Lio/sentry/time/Timing;
7628+
}
7629+
76247630
public abstract interface class io/sentry/time/Ticker {
76257631
public abstract fun tickNanos ()J
76267632
}

‎sentry/src/main/java/io/sentry/DateProviderEpochClock.java‎

Lines changed: 0 additions & 43 deletions
This file was deleted.

‎sentry/src/main/java/io/sentry/SentryOptions.java‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.sentry.time.ElapsedRealtimeClock;
2525
import io.sentry.time.EpochClock;
2626
import io.sentry.time.JavaElapsedRealtimeClock;
27+
import io.sentry.time.SystemEpochClock;
2728
import io.sentry.transport.ITransport;
2829
import io.sentry.transport.ITransportGate;
2930
import io.sentry.transport.NoOpEnvelopeCache;
@@ -529,7 +530,7 @@ public class SentryOptions {
529530
new LazyEvaluator<>(() -> new SentryAutoDateProvider());
530531

531532
private final @NotNull LazyEvaluator<EpochClock> epochClock =
532-
new LazyEvaluator<>(() -> new DateProviderEpochClock(this));
533+
new LazyEvaluator<>(() -> new SystemEpochClock(getElapsedRealtimeClock()));
533534

534535
private final @NotNull List<IPerformanceCollector> performanceCollectors = new ArrayList<>();
535536

@@ -3069,8 +3070,9 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) {
30693070
* Returns the wall clock, for stamping an instant that will be serialized or for starting
30703071
* something whose duration will be reported.
30713072
*
3072-
* <p>Backed by {@link #getDateProvider()}, so a provider set through {@link #setDateProvider} is
3073-
* honoured here too.
3073+
* <p>Independent of {@link #getDateProvider()}: the epoch values are the same, but an {@link
3074+
* io.sentry.time.Timing} measures on {@link #getElapsedRealtimeClock()} rather than on the {@link
3075+
* System#nanoTime()} tick a {@link SentryNanotimeDate} carries.
30743076
*/
30753077
@ApiStatus.Internal
30763078
public @NotNull EpochClock getEpochClock() {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.sentry.time;
2+
3+
import io.sentry.DateUtils;
4+
import java.time.Instant;
5+
import org.jetbrains.annotations.ApiStatus;
6+
7+
/**
8+
* Reads the epoch from {@link Instant}.
9+
*
10+
* <p>A class of its own so that the reference to {@code java.time} is loaded only where {@link
11+
* SystemEpochClock} decided to use it. Android's minSdk is below the API 26 that introduced {@code
12+
* Instant}.
13+
*/
14+
@ApiStatus.Internal
15+
@SuppressWarnings("NewApi")
16+
final class InstantEpochNanos {
17+
18+
private InstantEpochNanos() {}
19+
20+
static long read() {
21+
final Instant now = Instant.now();
22+
// No long overflow until year 2262
23+
return DateUtils.secondsToNanos(now.getEpochSecond()) + now.getNano();
24+
}
25+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.sentry.time;
2+
3+
import io.sentry.DateUtils;
4+
import io.sentry.util.Platform;
5+
import org.jetbrains.annotations.ApiStatus;
6+
import org.jetbrains.annotations.NotNull;
7+
8+
/**
9+
* The {@link EpochClock} backed by the system wall clock.
10+
*
11+
* <p>{@link #now()} reads the epoch at the best precision the platform offers: {@link
12+
* java.time.Instant} where it is sub-millisecond, and {@link System#currentTimeMillis()} everywhere
13+
* else. Android is always the latter — {@code Instant} is millisecond-granular there whether or not
14+
* the build desugars it, see https://github.com/getsentry/sentry-java/pull/2451.
15+
*
16+
* <p>Millisecond anchors are not the precision loss they look like, because nothing here measures
17+
* with them. A {@link Timing} reports its duration from a {@link Stopwatch}, so the nanosecond
18+
* precision that matters comes off the monotonic clock and only the anchor is coarse.
19+
*/
20+
@ApiStatus.Internal
21+
public final class SystemEpochClock implements EpochClock {
22+
23+
private static final boolean INSTANT_IS_SUB_MILLISECOND =
24+
Platform.isJvm() && Platform.isJavaNinePlus();
25+
26+
private final @NotNull ElapsedRealtimeClock clock;
27+
28+
public SystemEpochClock(final @NotNull ElapsedRealtimeClock clock) {
29+
this.clock = clock;
30+
}
31+
32+
@Override
33+
public @NotNull Timestamp now() {
34+
return Timestamp.ofEpochNanos(
35+
INSTANT_IS_SUB_MILLISECOND
36+
? InstantEpochNanos.read()
37+
: DateUtils.millisToNanos(System.currentTimeMillis()));
38+
}
39+
40+
@Override
41+
public @NotNull Timing start() {
42+
return Timing.started(now(), clock);
43+
}
44+
}

‎sentry/src/test/java/io/sentry/DateProviderEpochClockTest.kt‎

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.sentry.time
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import java.util.concurrent.TimeUnit.MILLISECONDS
5+
import kotlin.test.Test
6+
7+
class SystemEpochClockTest {
8+
@Test
9+
fun `now reads the system wall clock`() {
10+
val clock = SystemEpochClock(TestTicker())
11+
val before = MILLISECONDS.toNanos(System.currentTimeMillis())
12+
val now = clock.now().epochNanos()
13+
val after = MILLISECONDS.toNanos(System.currentTimeMillis())
14+
15+
// the bounds are millisecond-truncated, so now() may sit up to a millisecond past `after`
16+
assertThat(now).isAtLeast(before)
17+
assertThat(now).isAtMost(after + MILLISECONDS.toNanos(1))
18+
}
19+
20+
@Test
21+
fun `start anchors on the wall clock and measures on the supplied clock`() {
22+
val ticker = TestTicker()
23+
val clock = SystemEpochClock(ticker)
24+
val before = MILLISECONDS.toNanos(System.currentTimeMillis())
25+
26+
val timing = clock.start()
27+
ticker.advance(120, MILLISECONDS)
28+
29+
assertThat(timing.start().epochNanos()).isAtLeast(before)
30+
assertThat(timing.durationNanos()).isEqualTo(MILLISECONDS.toNanos(120))
31+
assertThat(timing.end().epochNanos())
32+
.isEqualTo(timing.start().epochNanos() + MILLISECONDS.toNanos(120))
33+
}
34+
}

0 commit comments

Comments
 (0)