Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
- `SentryTraced` now checks for its owning transaction dynamically rather than once per app process. The latter caused `SentryTraced` spans to be dropped process-wide once the original transaction finished ([#6057](https://github.com/getsentry/sentry-java/pull/6057))
- Fix typos in Spring GraphQL integration names (`GrahQL` to `GraphQL`) ([#6061](https://github.com/getsentry/sentry-java/pull/6061))

### Internal

- Add an internal `MonotonicTicker` abstraction with `Deadline` and `Stopwatch` primitives ([#6028](https://github.com/getsentry/sentry-java/pull/6028))

## 8.55.0

### Features
Expand Down
1 change: 1 addition & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;
public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader;
public fun getFrameMetricsCollector ()Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;
public fun getMonotonicTicker ()Lio/sentry/time/MonotonicTicker;
public fun getNativeSdkName ()Ljava/lang/String;
public fun getNdkAppHangTimeoutIntervalMillis ()J
public fun getNdkHandlerStrategy ()I
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.SpanStatus;
import io.sentry.android.core.internal.time.AndroidMonotonicTicker;
import io.sentry.android.core.internal.util.RootChecker;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.protocol.Mechanism;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryId;
import io.sentry.time.MonotonicTicker;
import io.sentry.util.SampleRateUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -889,6 +891,12 @@ public void setEnableAnrFingerprinting(final boolean enableAnrFingerprinting) {
this.enableAnrFingerprinting = enableAnrFingerprinting;
}

@Override
@ApiStatus.Internal
public @NotNull MonotonicTicker getMonotonicTicker() {
return AndroidMonotonicTicker.getInstance();
}

static class AndroidUserFeedbackFormHandler implements SentryFeedbackOptions.IFormHandler {
@Override
public void showForm(
Expand Down
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.MonotonicTicker;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* {@link MonotonicTicker} 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 AndroidMonotonicTicker implements MonotonicTicker {

private static final AndroidMonotonicTicker instance = new AndroidMonotonicTicker();

public static @NotNull MonotonicTicker getInstance() {
return instance;
}

private AndroidMonotonicTicker() {}

@Override
public long tickNanos() {
return SystemClock.elapsedRealtimeNanos();
Comment thread
runningcode marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.sentry.time

import java.util.concurrent.TimeUnit

/**
* A [MonotonicTicker] 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 ticker is off by a factor of a million and still compiles, whereas `advance(1001,
* MILLISECONDS)` cannot be.
*/
class TestMonotonicTicker(private var nanos: Long = 0) : MonotonicTicker {
override fun tickNanos(): Long = nanos

fun advance(amount: Long, unit: TimeUnit) {
nanos += unit.toNanos(amount)
}
}
24 changes: 24 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -3740,6 +3740,7 @@ public class io/sentry/SentryOptions {
public fun getMaxTraceFileSize ()J
public fun getMetrics ()Lio/sentry/SentryOptions$Metrics;
public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader;
public fun getMonotonicTicker ()Lio/sentry/time/MonotonicTicker;
public fun getOnDiscard ()Lio/sentry/SentryOptions$OnDiscardCallback;
public fun getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback;
public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode;
Expand Down Expand Up @@ -7597,6 +7598,29 @@ public final class io/sentry/rrweb/RRWebVideoEvent$JsonKeys {
public fun <init> ()V
}

public final class io/sentry/time/Deadline {
public static fun after (Lio/sentry/time/MonotonicTicker;JLjava/util/concurrent/TimeUnit;)Lio/sentry/time/Deadline;
public fun hasPassed ()Z
public fun isAfter (Lio/sentry/time/Deadline;)Z
public static fun passed (Lio/sentry/time/MonotonicTicker;)Lio/sentry/time/Deadline;
public fun remaining (Ljava/util/concurrent/TimeUnit;)J
}

public final class io/sentry/time/JavaMonotonicTicker : io/sentry/time/MonotonicTicker {
public static fun getInstance ()Lio/sentry/time/MonotonicTicker;
public fun tickNanos ()J
}

public abstract interface class io/sentry/time/MonotonicTicker {
public abstract fun tickNanos ()J
}

public final class io/sentry/time/Stopwatch {
public fun elapsed (Ljava/util/concurrent/TimeUnit;)J
public fun elapsedNanos ()J
public static fun started (Lio/sentry/time/MonotonicTicker;)Lio/sentry/time/Stopwatch;
}

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
14 changes: 14 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import io.sentry.metrics.IMetricsBatchProcessorFactory;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryTransaction;
import io.sentry.time.JavaMonotonicTicker;
import io.sentry.time.MonotonicTicker;
import io.sentry.transport.ITransport;
import io.sentry.transport.ITransportGate;
import io.sentry.transport.NoOpEnvelopeCache;
Expand Down Expand Up @@ -3059,6 +3061,18 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) {
this.dateProvider.setValue(dateProvider);
}

/**
* Returns the ticker used to measure elapsed time, such as rate-limit windows, cache expiry and
* ANR thresholds.
*
* <p>Android overrides this with a {@code SystemClock.elapsedRealtimeNanos()}-backed ticker,
* which this module cannot reference. On the JVM there is no suspend state to account for.
*/
@ApiStatus.Internal
public @NotNull MonotonicTicker getMonotonicTicker() {
return JavaMonotonicTicker.getInstance();
}

/**
* Adds a ICollector.
*
Expand Down
89 changes: 89 additions & 0 deletions sentry/src/main/java/io/sentry/time/Deadline.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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 MonotonicTicker}.
*
* <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 {
Comment thread
runningcode marked this conversation as resolved.

private final @NotNull MonotonicTicker ticker;
private final long deadlineNanos;

private Deadline(final @NotNull MonotonicTicker ticker, final long deadlineNanos) {
this.ticker = ticker;
this.deadlineNanos = deadlineNanos;
}

/**
* A deadline {@code amount} of {@code unit} from now.
*
* @throws IllegalArgumentException if {@code amount} is negative. A deadline that starts out in
* the past is a sign error at the call site; {@link #passed} says it deliberately.
*/
public static @NotNull Deadline after(
final @NotNull MonotonicTicker ticker, final long amount, final @NotNull TimeUnit unit) {
if (amount < 0) {
throw new IllegalArgumentException("Deadline amount must not be negative, but was " + amount);
}
return new Deadline(ticker, ticker.tickNanos() + unit.toNanos(amount));
}

/**
* A deadline that has already passed.
*
* <p>Saves callers from reserving a tick value to mean "not set yet": {@code 0} is a real and
* very recent instant on a boot-relative ticker, so a field left at {@code 0} reads as freshly
* set rather than as unset.
*/
public static @NotNull Deadline passed(final @NotNull MonotonicTicker ticker) {
return new Deadline(ticker, ticker.tickNanos());
}

public boolean hasPassed() {
// Subtraction rather than `<`: a tick origin is arbitrary, may be negative, and may wrap.
return ticker.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 - ticker.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 tickers, whose origins
* are unrelated and whose ticks are therefore not comparable.
*/
public boolean isAfter(final @NotNull Deadline other) {
if (ticker != other.ticker) {
throw new IllegalArgumentException(
Comment thread
runningcode marked this conversation as resolved.
"Cannot compare deadlines from different tickers: "
+ ticker.getClass().getName()
+ " and "
+ other.ticker.getClass().getName());
}
return deadlineNanos - other.deadlineNanos > 0;
}
}
22 changes: 22 additions & 0 deletions sentry/src/main/java/io/sentry/time/JavaMonotonicTicker.java
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 MonotonicTicker} backed by {@link System#nanoTime()}. */
@ApiStatus.Internal
public final class JavaMonotonicTicker implements MonotonicTicker {

private static final JavaMonotonicTicker instance = new JavaMonotonicTicker();

public static @NotNull MonotonicTicker getInstance() {
return instance;
}

private JavaMonotonicTicker() {}

@Override
public long tickNanos() {
return System.nanoTime();
}
}
22 changes: 22 additions & 0 deletions sentry/src/main/java/io/sentry/time/MonotonicTicker.java
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,
Comment thread
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 ticker.
*
* <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 MonotonicTicker {
long tickNanos();
}
35 changes: 35 additions & 0 deletions sentry/src/main/java/io/sentry/time/Stopwatch.java
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 MonotonicTicker}.
*
* <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 MonotonicTicker ticker;
private final long startNanos;

private Stopwatch(final @NotNull MonotonicTicker ticker) {
this.ticker = ticker;
this.startNanos = ticker.tickNanos();
}

public static @NotNull Stopwatch started(final @NotNull MonotonicTicker ticker) {
return new Stopwatch(ticker);
}

Comment thread
runningcode marked this conversation as resolved.
public long elapsedNanos() {
return ticker.tickNanos() - startNanos;
}

public long elapsed(final @NotNull TimeUnit unit) {
return unit.convert(elapsedNanos(), TimeUnit.NANOSECONDS);
}
}
Loading
Loading