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
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