Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
9 changes: 9 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun getNativeSdkName ()Ljava/lang/String;
public fun getNdkHandlerStrategy ()I
public fun getScreenshot ()Lio/sentry/android/core/SentryScreenshotOptions;
public fun getSpanFrameMetricsCollector ()Lio/sentry/android/core/SpanFrameMetricsCollector;
public fun getStartupCrashDurationThresholdMillis ()J
public fun isAnrEnabled ()Z
public fun isAnrProfilingEnabled ()Z
Expand Down Expand Up @@ -428,13 +429,20 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setNativeSdkName (Ljava/lang/String;)V
public fun setReportHistoricalAnrs (Z)V
public fun setReportHistoricalTombstones (Z)V
public fun setSpanFrameMetricsCollector (Lio/sentry/android/core/SpanFrameMetricsCollector;)V
public fun setTombstoneEnabled (Z)V
}

public abstract interface class io/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback {
public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;Z)Z
}

public final class io/sentry/android/core/SentryFramesDelayResult {
public fun <init> (DI)V
public fun getDelaySeconds ()D
public fun getFramesContributingToDelayCount ()I
}

public final class io/sentry/android/core/SentryInitProvider {
public fun <init> ()V
public fun attachInfo (Landroid/content/Context;Landroid/content/pm/ProviderInfo;)V
Expand Down Expand Up @@ -520,6 +528,7 @@ public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerfo
protected final field lock Lio/sentry/util/AutoClosableReentrantLock;
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V
public fun clear ()V
public fun getFramesDelay (JJ)Lio/sentry/android/core/SentryFramesDelayResult;
public fun onFrameMetricCollected (JJJJZZF)V
public fun onSpanFinished (Lio/sentry/ISpan;)V
public fun onSpanStarted (Lio/sentry/ISpan;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,14 @@ static void initializeIntegrationsAndProcessors(
options.addPerformanceCollector(new AndroidCpuCollector(options.getLogger()));

if (options.isEnablePerformanceV2()) {
options.addPerformanceCollector(
final SpanFrameMetricsCollector spanFrameMetricsCollector =
new SpanFrameMetricsCollector(
options,
Objects.requireNonNull(
options.getFrameMetricsCollector(),
"options.getFrameMetricsCollector is required")));
"options.getFrameMetricsCollector is required"));
options.addPerformanceCollector(spanFrameMetricsCollector);
options.setSpanFrameMetricsCollector(spanFrameMetricsCollector);
}
}
if (options.getCompositePerformanceCollector() instanceof NoOpCompositePerformanceCollector) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ public interface BeforeCaptureCallback {

private @Nullable SentryFrameMetricsCollector frameMetricsCollector;

private @Nullable SpanFrameMetricsCollector spanFrameMetricsCollector;

private boolean enableTombstone = false;

/**
Expand Down Expand Up @@ -674,6 +676,17 @@ public void setFrameMetricsCollector(
this.frameMetricsCollector = frameMetricsCollector;
}

@ApiStatus.Internal
public @Nullable SpanFrameMetricsCollector getSpanFrameMetricsCollector() {
return spanFrameMetricsCollector;
}

@ApiStatus.Internal
public void setSpanFrameMetricsCollector(
final @Nullable SpanFrameMetricsCollector spanFrameMetricsCollector) {
this.spanFrameMetricsCollector = spanFrameMetricsCollector;
}

public boolean isEnableAutoTraceIdGeneration() {
return enableAutoTraceIdGeneration;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.sentry.android.core;

import org.jetbrains.annotations.ApiStatus;

/** Result of querying frame delay for a given time range. */
@ApiStatus.Internal
public final class SentryFramesDelayResult {

private final double delaySeconds;
private final int framesContributingToDelayCount;

public SentryFramesDelayResult(
final double delaySeconds, final int framesContributingToDelayCount) {
this.delaySeconds = delaySeconds;
this.framesContributingToDelayCount = framesContributingToDelayCount;
}

/**
* @return the total frame delay in seconds, or -1 if incalculable (e.g. no frame data available)
*/
public double getDelaySeconds() {
return delaySeconds;
}

/**
* @return the number of frames that contributed to the delay (slow + frozen frames)
*/
public int getFramesContributingToDelayCount() {
return framesContributingToDelayCount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,49 +152,11 @@ private void captureFrameMetrics(@NotNull final ISpan span) {
return;
}

final @NotNull SentryFrameMetrics frameMetrics = new SentryFrameMetrics();

long frameDurationNanos = lastKnownFrameDurationNanos;

if (!frames.isEmpty()) {
// determine relevant start in frames list
final Iterator<Frame> iterator = frames.tailSet(new Frame(spanStartNanos)).iterator();

//noinspection WhileLoopReplaceableByForEach
while (iterator.hasNext()) {
final @NotNull Frame frame = iterator.next();

if (frame.startNanos > spanEndNanos) {
break;
}

if (frame.startNanos >= spanStartNanos && frame.endNanos <= spanEndNanos) {
// if the frame is contained within the span, add it 1:1 to the span metrics
frameMetrics.addFrame(
frame.durationNanos, frame.delayNanos, frame.isSlow, frame.isFrozen);
} else if ((spanStartNanos > frame.startNanos && spanStartNanos < frame.endNanos)
|| (spanEndNanos > frame.startNanos && spanEndNanos < frame.endNanos)) {
// span start or end are within frame
// calculate the intersection
final long durationBeforeSpan = Math.max(0, spanStartNanos - frame.startNanos);
final long delayBeforeSpan =
Math.max(0, durationBeforeSpan - frame.expectedDurationNanos);
final long delayWithinSpan =
Math.min(frame.delayNanos - delayBeforeSpan, spanDurationNanos);

final long frameStart = Math.max(spanStartNanos, frame.startNanos);
final long frameEnd = Math.min(spanEndNanos, frame.endNanos);
final long frameDuration = frameEnd - frameStart;
frameMetrics.addFrame(
frameDuration,
delayWithinSpan,
SentryFrameMetricsCollector.isSlow(frameDuration, frame.expectedDurationNanos),
SentryFrameMetricsCollector.isFrozen(frameDuration));
}

frameDurationNanos = frame.expectedDurationNanos;
}
}
// effectiveFrameDuration tracks the expected frame duration of the last frame
// iterated within the span's time range, falling back to lastKnownFrameDurationNanos
final long[] effectiveFrameDuration = {lastKnownFrameDurationNanos};
final @NotNull SentryFrameMetrics frameMetrics =
calculateFrameMetrics(spanStartNanos, spanEndNanos, effectiveFrameDuration);

int totalFrameCount = frameMetrics.getSlowFrozenFrameCount();

Expand All @@ -204,9 +166,9 @@ private void captureFrameMetrics(@NotNull final ISpan span) {
if (nextScheduledFrameNanos != -1) {
totalFrameCount +=
addPendingFrameDelay(
frameMetrics, frameDurationNanos, spanEndNanos, nextScheduledFrameNanos);
frameMetrics, effectiveFrameDuration[0], spanEndNanos, nextScheduledFrameNanos);
totalFrameCount +=
interpolateFrameCount(frameMetrics, frameDurationNanos, spanDurationNanos);
interpolateFrameCount(frameMetrics, effectiveFrameDuration[0], spanDurationNanos);
}
final long frameDelayNanos =
frameMetrics.getSlowFrameDelayNanos() + frameMetrics.getFrozenFrameDelayNanos();
Expand All @@ -226,6 +188,100 @@ private void captureFrameMetrics(@NotNull final ISpan span) {
}
}

/**
* Queries the frame delay for a given time range, without requiring an active span.
*
* <p>This is useful for external consumers (e.g. React Native SDK) that need to query frame delay
* for an arbitrary time range without registering their own frame listener.
*
* @param startSystemNanos start of the time range in {@link System#nanoTime()} units
* @param endSystemNanos end of the time range in {@link System#nanoTime()} units
* @return a {@link SentryFramesDelayResult} with the delay in seconds and the number of frames
* contributing to delay, or a result with delaySeconds=-1 if incalculable
*/
public @NotNull SentryFramesDelayResult getFramesDelay(
final long startSystemNanos, final long endSystemNanos) {
if (!enabled) {
return new SentryFramesDelayResult(-1, 0);
}

final long durationNanos = endSystemNanos - startSystemNanos;
if (durationNanos <= 0) {
return new SentryFramesDelayResult(-1, 0);
}

final long[] effectiveFrameDuration = {lastKnownFrameDurationNanos};
final @NotNull SentryFrameMetrics frameMetrics =
calculateFrameMetrics(startSystemNanos, endSystemNanos, effectiveFrameDuration);

final long nextScheduledFrameNanos = frameMetricsCollector.getLastKnownFrameStartTimeNanos();
if (nextScheduledFrameNanos != -1) {
addPendingFrameDelay(
frameMetrics, effectiveFrameDuration[0], endSystemNanos, nextScheduledFrameNanos);
}

final long frameDelayNanos =
frameMetrics.getSlowFrameDelayNanos() + frameMetrics.getFrozenFrameDelayNanos();
final double frameDelayInSeconds = frameDelayNanos / 1e9d;

return new SentryFramesDelayResult(frameDelayInSeconds, frameMetrics.getSlowFrozenFrameCount());
}

/**
* Calculates frame metrics for a given time range by iterating over stored frames and handling
* partial overlaps at the boundaries.
*
* @param startNanos start of the time range
* @param endNanos end of the time range
* @param effectiveFrameDuration a single-element array that will be updated with the expected
* frame duration of the last iterated frame (used for pending delay / interpolation)
*/
private @NotNull SentryFrameMetrics calculateFrameMetrics(
final long startNanos, final long endNanos, final long @NotNull [] effectiveFrameDuration) {
final long durationNanos = endNanos - startNanos;
final @NotNull SentryFrameMetrics frameMetrics = new SentryFrameMetrics();

if (!frames.isEmpty()) {
final Iterator<Frame> iterator = frames.tailSet(new Frame(startNanos)).iterator();

//noinspection WhileLoopReplaceableByForEach
while (iterator.hasNext()) {
final @NotNull Frame frame = iterator.next();

if (frame.startNanos > endNanos) {
break;
}

if (frame.startNanos >= startNanos && frame.endNanos <= endNanos) {
// if the frame is contained within the range, add it 1:1
frameMetrics.addFrame(
frame.durationNanos, frame.delayNanos, frame.isSlow, frame.isFrozen);
} else if ((startNanos > frame.startNanos && startNanos < frame.endNanos)
|| (endNanos > frame.startNanos && endNanos < frame.endNanos)) {
// range start or end are within frame โ€” calculate the intersection
final long durationBeforeRange = Math.max(0, startNanos - frame.startNanos);
final long delayBeforeRange =
Math.max(0, durationBeforeRange - frame.expectedDurationNanos);
final long delayWithinRange =
Math.min(frame.delayNanos - delayBeforeRange, durationNanos);

final long frameStart = Math.max(startNanos, frame.startNanos);
final long frameEnd = Math.min(endNanos, frame.endNanos);
final long frameDuration = frameEnd - frameStart;
frameMetrics.addFrame(
frameDuration,
delayWithinRange,
SentryFrameMetricsCollector.isSlow(frameDuration, frame.expectedDurationNanos),
SentryFrameMetricsCollector.isFrozen(frameDuration));
}

effectiveFrameDuration[0] = frame.expectedDurationNanos;
}
}

return frameMetrics;
}

@Override
public void clear() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
Expand Down
Loading
Loading