Skip to content

Commit 0b7e6e7

Browse files
runningcodeclaude
andcommitted
ref(checkin): Measure check-in durations with Stopwatch (JAVA-576)
All four check-in paths kept a `long startTime = System.nanoTime()` and subtracted it in a finally block: CheckInUtils and the SentryCheckInAdvice in sentry-spring, sentry-spring-jakarta and sentry-spring-7. They now share one Stopwatch, which takes its clock from the options. Inert on every path that ships. The options hand the check-in path a JavaMonotonicTicker, whose tickNanos() is System.nanoTime(), so the serialized duration is unchanged to the bit. A new test brackets a tick between two nanoTime readings, pinning the source rather than merely the resolution. SentryAndroidOptions does override the ticker with a boottime one, but nothing reaches it from here: check-ins have no Android integration -- no WorkManager, JobScheduler or AlarmManager wrapper, no manifest option, no Android docs. Measuring a check-in across device sleep is a change for the next major. An uninitialized SDK still reaches a clock: NoOpScopes.getOptions() hands back empty options, whose ticker is the JVM one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5c1467e commit 0b7e6e7

5 files changed

Lines changed: 33 additions & 8 deletions

File tree

‎sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdvice.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.sentry.ScopesAdapter;
1010
import io.sentry.SentryLevel;
1111
import io.sentry.protocol.SentryId;
12+
import io.sentry.time.Stopwatch;
1213
import io.sentry.util.Objects;
1314
import io.sentry.util.TracingUtils;
1415
import java.lang.reflect.Method;
@@ -91,7 +92,8 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
9192
TracingUtils.startNewTrace(scopes);
9293

9394
@Nullable SentryId checkInId = null;
94-
final long startTime = System.nanoTime();
95+
final @NotNull Stopwatch stopwatch =
96+
Stopwatch.started(scopes.getOptions().getMonotonicTicker());
9597
boolean didError = false;
9698

9799
try {
@@ -105,7 +107,7 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
105107
} finally {
106108
final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK;
107109
CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status);
108-
checkIn.setDuration(DateUtils.nanosToSeconds(System.nanoTime() - startTime));
110+
checkIn.setDuration(DateUtils.nanosToSeconds(stopwatch.elapsedNanos()));
109111
scopes.captureCheckIn(checkIn);
110112
}
111113
}

‎sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.sentry.ScopesAdapter;
1010
import io.sentry.SentryLevel;
1111
import io.sentry.protocol.SentryId;
12+
import io.sentry.time.Stopwatch;
1213
import io.sentry.util.Objects;
1314
import io.sentry.util.TracingUtils;
1415
import java.lang.reflect.Method;
@@ -91,7 +92,8 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
9192
TracingUtils.startNewTrace(scopes);
9293

9394
@Nullable SentryId checkInId = null;
94-
final long startTime = System.nanoTime();
95+
final @NotNull Stopwatch stopwatch =
96+
Stopwatch.started(scopes.getOptions().getMonotonicTicker());
9597
boolean didError = false;
9698

9799
try {
@@ -105,7 +107,7 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
105107
} finally {
106108
final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK;
107109
CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status);
108-
checkIn.setDuration(DateUtils.nanosToSeconds(System.nanoTime() - startTime));
110+
checkIn.setDuration(DateUtils.nanosToSeconds(stopwatch.elapsedNanos()));
109111
scopes.captureCheckIn(checkIn);
110112
}
111113
}

‎sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.sentry.ScopesAdapter;
1010
import io.sentry.SentryLevel;
1111
import io.sentry.protocol.SentryId;
12+
import io.sentry.time.Stopwatch;
1213
import io.sentry.util.Objects;
1314
import io.sentry.util.TracingUtils;
1415
import java.lang.reflect.Method;
@@ -94,7 +95,8 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
9495
TracingUtils.startNewTrace(scopes);
9596

9697
@Nullable SentryId checkInId = null;
97-
final long startTime = System.nanoTime();
98+
final @NotNull Stopwatch stopwatch =
99+
Stopwatch.started(scopes.getOptions().getMonotonicTicker());
98100
boolean didError = false;
99101

100102
try {
@@ -108,7 +110,7 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl
108110
} finally {
109111
final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK;
110112
CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status);
111-
checkIn.setDuration(DateUtils.nanosToSeconds(System.nanoTime() - startTime));
113+
checkIn.setDuration(DateUtils.nanosToSeconds(stopwatch.elapsedNanos()));
112114
scopes.captureCheckIn(checkIn);
113115
}
114116
}

‎sentry/src/main/java/io/sentry/util/CheckInUtils.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.sentry.MonitorConfig;
1010
import io.sentry.Sentry;
1111
import io.sentry.protocol.SentryId;
12+
import io.sentry.time.Stopwatch;
1213
import java.util.List;
1314
import java.util.concurrent.Callable;
1415
import org.jetbrains.annotations.ApiStatus;
@@ -37,7 +38,8 @@ public static <U> U withCheckIn(
3738
try (final @NotNull ISentryLifecycleToken ignored =
3839
Sentry.forkedScopes("CheckInUtils").makeCurrent()) {
3940
final @NotNull IScopes scopes = Sentry.getCurrentScopes();
40-
final long startTime = System.nanoTime();
41+
final @NotNull Stopwatch stopwatch =
42+
Stopwatch.started(scopes.getOptions().getMonotonicTicker());
4143
boolean didError = false;
4244

4345
TracingUtils.startNewTrace(scopes);
@@ -61,7 +63,7 @@ public static <U> U withCheckIn(
6163
if (environment != null) {
6264
checkIn.setEnvironment(environment);
6365
}
64-
checkIn.setDuration(DateUtils.nanosToSeconds(System.nanoTime() - startTime));
66+
checkIn.setDuration(DateUtils.nanosToSeconds(stopwatch.elapsedNanos()));
6567
scopes.captureCheckIn(checkIn);
6668
}
6769
}

‎sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.sentry.util
22

3+
import com.google.common.truth.Truth.assertThat
34
import io.sentry.CheckInStatus
45
import io.sentry.FilterString
56
import io.sentry.IScopes
@@ -143,6 +144,7 @@ class CheckInUtilsTest {
143144
sentry.`when`<Any> { Sentry.forkedScopes(any()) }.then { scopes.forkedScopes("test") }
144145
whenever(scopes.forkedScopes(any())).thenReturn(scopes)
145146
whenever(scopes.makeCurrent()).thenReturn(lifecycleToken)
147+
whenever(scopes.options).thenReturn(SentryOptions())
146148

147149
try {
148150
CheckInUtils.withCheckIn("monitor-1") { throw RuntimeException("thrown on purpose") }
@@ -312,4 +314,19 @@ class CheckInUtilsTest {
312314
assertEquals("America/Los_Angeles", monitorConfig.timezone)
313315
}
314316
}
317+
318+
@Test
319+
fun `resolves a ticker that ticks on System nanoTime`() {
320+
// The clock the check-in path reads. Bracketing a tick between two System.nanoTime() readings
321+
// pins the source, not just the resolution: a ticker on any other monotonic source reports a
322+
// different epoch and would fall outside the bracket.
323+
val ticker = SentryOptions().monotonicTicker
324+
325+
val before = System.nanoTime()
326+
val tick = ticker.tickNanos()
327+
val after = System.nanoTime()
328+
329+
assertThat(tick).isAtLeast(before)
330+
assertThat(tick).isAtMost(after)
331+
}
315332
}

0 commit comments

Comments
 (0)