Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Internal

- Add an internal `MonotonicClock` abstraction with `Deadline` and `Stopwatch` primitives ([#6028](https://github.com/getsentry/sentry-java/pull/6028))
- Add internal `Timestamp`, `Timing` and `EpochClock`, separating a serialized wall-clock instant from a monotonically measured duration ([#6045](https://github.com/getsentry/sentry-java/pull/6045))

## 8.55.0

Expand Down
28 changes: 28 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -3707,6 +3707,7 @@ public class io/sentry/SentryOptions {
public fun getEnvelopeDiskCache ()Lio/sentry/cache/IEnvelopeCache;
public fun getEnvelopeReader ()Lio/sentry/IEnvelopeReader;
public fun getEnvironment ()Ljava/lang/String;
public fun getEpochClock ()Lio/sentry/time/EpochClock;
public fun getEventProcessors ()Ljava/util/List;
public fun getExecutorService ()Lio/sentry/ISentryExecutorService;
public fun getExperimental ()Lio/sentry/ExperimentalOptions;
Expand Down Expand Up @@ -7606,6 +7607,11 @@ public final class io/sentry/time/Deadline {
public fun remaining (Ljava/util/concurrent/TimeUnit;)J
}

public abstract interface class io/sentry/time/EpochClock {
public abstract fun now ()Lio/sentry/time/Timestamp;
public abstract fun start ()Lio/sentry/time/Timing;
}

public final class io/sentry/time/JavaMonotonicClock : io/sentry/time/MonotonicClock {
public static fun getInstance ()Lio/sentry/time/MonotonicClock;
public fun tickNanos ()J
Expand All @@ -7621,6 +7627,28 @@ public final class io/sentry/time/Stopwatch {
public static fun started (Lio/sentry/time/MonotonicClock;)Lio/sentry/time/Stopwatch;
}

public final class io/sentry/time/SystemEpochClock : io/sentry/time/EpochClock {
public fun <init> (Lio/sentry/time/MonotonicClock;)V
public fun now ()Lio/sentry/time/Timestamp;
public fun start ()Lio/sentry/time/Timing;
}

public final class io/sentry/time/Timestamp {
public fun epochNanos ()J
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public static fun ofEpochNanos (J)Lio/sentry/time/Timestamp;
public fun plusNanos (J)Lio/sentry/time/Timestamp;
public fun toString ()Ljava/lang/String;
}

public final class io/sentry/time/Timing {
public fun durationNanos ()J
public fun end ()Lio/sentry/time/Timestamp;
public fun start ()Lio/sentry/time/Timestamp;
public static fun started (Lio/sentry/time/Timestamp;Lio/sentry/time/MonotonicClock;)Lio/sentry/time/Timing;
}

public final class io/sentry/transport/AsyncHttpTransport : io/sentry/transport/ITransport {
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/transport/RateLimiter;Lio/sentry/transport/ITransportGate;Lio/sentry/RequestDetails;)V
public fun <init> (Lio/sentry/transport/QueuedThreadPoolExecutor;Lio/sentry/SentryOptions;Lio/sentry/transport/RateLimiter;Lio/sentry/transport/ITransportGate;Lio/sentry/transport/HttpConnection;)V
Expand Down
18 changes: 18 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import io.sentry.metrics.IMetricsBatchProcessorFactory;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryTransaction;
import io.sentry.time.EpochClock;
import io.sentry.time.JavaMonotonicClock;
import io.sentry.time.MonotonicClock;
import io.sentry.time.SystemEpochClock;
import io.sentry.transport.ITransport;
import io.sentry.transport.ITransportGate;
import io.sentry.transport.NoOpEnvelopeCache;
Expand Down Expand Up @@ -527,6 +529,9 @@ public class SentryOptions {
private final @NotNull LazyEvaluator<SentryDateProvider> dateProvider =
new LazyEvaluator<>(() -> new SentryAutoDateProvider());

private final @NotNull LazyEvaluator<EpochClock> epochClock =
new LazyEvaluator<>(() -> new SystemEpochClock(getMonotonicClock()));

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

/** Performance collector that collect performance stats while transactions run. */
Expand Down Expand Up @@ -3061,6 +3066,19 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) {
this.dateProvider.setValue(dateProvider);
}

/**
* Returns the wall clock, for stamping an instant that will be serialized or for starting
* something whose duration will be reported.
*
* <p>Independent of {@link #getDateProvider()}: the epoch values are the same, but an {@link
* io.sentry.time.Timing} measures on {@link #getMonotonicClock()} rather than on the {@link
* System#nanoTime()} tick a {@link SentryNanotimeDate} carries.
*/
@ApiStatus.Internal
public @NotNull EpochClock getEpochClock() {
return epochClock.getValue();
}

/**
* Returns the clock used to measure elapsed time, such as rate-limit windows, cache expiry and
* ANR thresholds.
Expand Down
28 changes: 28 additions & 0 deletions sentry/src/main/java/io/sentry/time/EpochClock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.sentry.time;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* The source of wall-clock time.
*
* <p>Two entry points, because there are two questions and they need different answers. {@link
* #now()} stamps a moment โ€” an event, a breadcrumb, a session โ€” and hands back an instant with no
* arithmetic on it. {@link #start()} begins something whose duration will be reported, and hands
* back a {@link Timing} that measures on a monotonic clock.
*
* <p>The split is the point. A call site has to say which it is doing, and neither result can do
* the other's job, so a duration cannot quietly end up being the difference between two wall-clock
* readings.
*/
@ApiStatus.Internal
public interface EpochClock {

/** The current instant. Serialize it; do not subtract it from another one. */
@NotNull
Timestamp now();

/** Starts measuring now. */
@NotNull
Timing start();
}
25 changes: 25 additions & 0 deletions sentry/src/main/java/io/sentry/time/InstantEpochNanos.java
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 that 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() {
final Instant now = Instant.now();
// No long overflow until year 2262
return DateUtils.secondsToNanos(now.getEpochSecond()) + now.getNano();
}
}
44 changes: 44 additions & 0 deletions sentry/src/main/java/io/sentry/time/SystemEpochClock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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>{@link #now()} reads the epoch at the best precision the platform offers: {@link
* java.time.Instant} where it is sub-millisecond, and {@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>Millisecond anchors are not the precision loss they look like, because nothing here measures
* with them. A {@link Timing} reports its duration from a {@link Stopwatch}, so the nanosecond
* precision that matters comes off the monotonic clock and 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 final @NotNull MonotonicClock clock;

public SystemEpochClock(final @NotNull MonotonicClock clock) {
this.clock = clock;
}

@Override
public @NotNull Timestamp now() {
return Timestamp.ofEpochNanos(
INSTANT_IS_SUB_MILLISECOND
? InstantEpochNanos.read()
: DateUtils.millisToNanos(System.currentTimeMillis()));
}

@Override
public @NotNull Timing start() {
return Timing.started(now(), clock);
}
}
64 changes: 64 additions & 0 deletions sentry/src/main/java/io/sentry/time/Timestamp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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>The counterpart to {@link MonotonicClock}, and its opposite in every respect that matters: a
* timestamp is anchored to an epoch, so it is meaningful outside this process โ€” it can be
* serialized, stored, and compared against a value produced by another machine. A tick can do none
* of those things.
*
* <p>What a timestamp deliberately cannot do is measure an interval. Subtracting two wall-clock
* readings gives a duration that the device's clock can lengthen, shorten or make negative, so this
* type offers no arithmetic between instants. Pair it with a {@link Stopwatch} โ€” which is what
* {@link Timing} is โ€” and the duration comes from a monotonic clock instead.
*
* <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;
}

/** This instant moved forward by {@code nanos}, for deriving an end from a measured duration. */
public @NotNull Timestamp plusNanos(final long nanos) {
return new Timestamp(epochNanos + nanos);
}

@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 + '}';
}
}
55 changes: 55 additions & 0 deletions sentry/src/main/java/io/sentry/time/Timing.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.sentry.time;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* Something whose start instant is reported and whose duration is measured.
*
* <p>A span, a profile or a replay segment needs two facts that no single clock can supply: when it
* began, in epoch terms, so it can be lined up with events from other systems; and how long it
* took, measured on a clock that cannot jump. So this holds both โ€” a {@link Timestamp} anchor and a
* {@link Stopwatch} โ€” captured together, and derives the end from them.
*
* <p>That derivation is the reason the type exists. The span protocol carries a start and an end
* instant and no duration, so the server subtracts them; emitting a separately-read wall-clock end
* would throw away the monotonic measurement and report whatever the device's clock did in between.
* {@link #end()} is therefore the anchor plus the measured duration, computed in one place instead
* of at every call site.
*/
@ApiStatus.Internal
public final class Timing {

private final @NotNull Timestamp start;
private final @NotNull Stopwatch stopwatch;

private Timing(final @NotNull Timestamp start, final @NotNull Stopwatch stopwatch) {
this.start = start;
this.stopwatch = stopwatch;
}

/**
* Starts measuring, anchored at {@code start}.
*
* <p>{@link MonotonicClock} counts deep sleep, which is what pairing with a wall-clock anchor
* requires: an interval that excluded it would produce an {@link #end()} that falls further
* behind real time the longer the device sleeps.
*/
public static @NotNull Timing started(
final @NotNull Timestamp start, final @NotNull MonotonicClock clock) {
return new Timing(start, Stopwatch.started(clock));
}

public @NotNull Timestamp start() {
return start;
}

public long durationNanos() {
return stopwatch.elapsedNanos();
}

/** The anchor plus the duration measured so far. */
public @NotNull Timestamp end() {
return start.plusNanos(durationNanos());
}
}
34 changes: 34 additions & 0 deletions sentry/src/test/java/io/sentry/time/SystemEpochClockTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 clock = SystemEpochClock(TestMonotonicClock())
val before = MILLISECONDS.toNanos(System.currentTimeMillis())
val now = clock.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))
}

@Test
fun `start anchors on the wall clock and measures on the supplied clock`() {
val ticker = TestMonotonicClock()
val clock = SystemEpochClock(ticker)
val before = MILLISECONDS.toNanos(System.currentTimeMillis())

val timing = clock.start()
ticker.advance(120, MILLISECONDS)

assertThat(timing.start().epochNanos()).isAtLeast(before)
assertThat(timing.durationNanos()).isEqualTo(MILLISECONDS.toNanos(120))
assertThat(timing.end().epochNanos())
.isEqualTo(timing.start().epochNanos() + MILLISECONDS.toNanos(120))
}
}
32 changes: 32 additions & 0 deletions sentry/src/test/java/io/sentry/time/TimestampTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.sentry.time

import java.util.concurrent.TimeUnit.MILLISECONDS
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

class TimestampTest {
@Test
fun `keeps the epoch value it was given`() {
assertEquals(
1_700_000_000_000_000_000,
Timestamp.ofEpochNanos(1_700_000_000_000_000_000).epochNanos(),
)
}

@Test
fun `moves forward without mutating the original`() {
val start = Timestamp.ofEpochNanos(1_000)
val later = start.plusNanos(MILLISECONDS.toNanos(5))

assertEquals(1_000, start.epochNanos())
assertEquals(1_000 + MILLISECONDS.toNanos(5), later.epochNanos())
}

@Test
fun `compares by value`() {
assertEquals(Timestamp.ofEpochNanos(42), Timestamp.ofEpochNanos(42))
assertEquals(Timestamp.ofEpochNanos(42).hashCode(), Timestamp.ofEpochNanos(42).hashCode())
assertNotEquals(Timestamp.ofEpochNanos(42), Timestamp.ofEpochNanos(43))
}
}
Loading
Loading