From 5a75171bdf9e21615488c22e725d0c985a893640 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 30 Apr 2026 11:07:31 +0200 Subject: [PATCH 1/3] perf(android): Replace RNSentryFrameDelayCollector with sentry-java getFramesDelay API Uses the new queryable `SentryFrameMetricsCollector.getFramesDelay()` API from sentry-java (getsentry/sentry-java#5248, commit 61659b6) instead of maintaining a custom listener-based collector. Closes #5908 Co-Authored-By: Claude Opus 4.6 --- .../react/RNSentryFrameDelayCollector.java | 128 ------------------ .../io/sentry/react/RNSentryModuleImpl.java | 42 ++++-- 2 files changed, 34 insertions(+), 136 deletions(-) delete mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java deleted file mode 100644 index a3295ed4b4..0000000000 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryFrameDelayCollector.java +++ /dev/null @@ -1,128 +0,0 @@ -package io.sentry.react; - -import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import org.jetbrains.annotations.Nullable; - -/** - * Collects per-frame delay data from {@link SentryFrameMetricsCollector} and provides a method to - * query the accumulated delay within a given time range. - * - *

This is a temporary solution until sentry-java exposes a queryable API for frames delay - * (similar to sentry-cocoa's getFramesDelaySPI). - */ -public class RNSentryFrameDelayCollector - implements SentryFrameMetricsCollector.FrameMetricsCollectorListener { - - private static final long MAX_FRAME_AGE_NANOS = 5L * 60 * 1_000_000_000L; // 5 minutes - - private final List frames = new CopyOnWriteArrayList<>(); - - private @Nullable String listenerId; - private @Nullable SentryFrameMetricsCollector collector; - - /** - * Starts collecting frame delay data from the given collector. - * - * @return true if collection was started successfully - */ - public boolean start(@Nullable SentryFrameMetricsCollector frameMetricsCollector) { - if (frameMetricsCollector == null) { - return false; - } - stop(); - this.collector = frameMetricsCollector; - this.listenerId = frameMetricsCollector.startCollection(this); - return this.listenerId != null; - } - - /** Stops collecting frame delay data. */ - public void stop() { - if (collector != null && listenerId != null) { - collector.stopCollection(listenerId); - listenerId = null; - collector = null; - } - frames.clear(); - } - - @Override - public void onFrameMetricCollected( - long frameStartNanos, - long frameEndNanos, - long durationNanos, - long delayNanos, - boolean isSlow, - boolean isFrozen, - float refreshRate) { - if (delayNanos <= 0) { - return; - } - frames.add(new FrameRecord(frameStartNanos, frameEndNanos, delayNanos)); - pruneOldFrames(frameEndNanos); - } - - /** - * Returns the total frames delay in seconds for the given time range. - * - *

Handles partial overlap: if a frame's delay period partially falls within the query range, - * only the overlapping portion is counted. - * - * @param startNanos start of the query range in system nanos (e.g., System.nanoTime()) - * @param endNanos end of the query range in system nanos - * @return delay in seconds, or -1 if no data is available - */ - public double getFramesDelay(long startNanos, long endNanos) { - if (startNanos >= endNanos) { - return -1; - } - - long totalDelayNanos = 0; - - for (FrameRecord frame : frames) { - if (frame.endNanos <= startNanos) { - continue; - } - if (frame.startNanos >= endNanos) { - break; - } - - // The delay portion of a frame is at the end of the frame duration. - // delayStart = frameEnd - delay, delayEnd = frameEnd - long delayStart = frame.endNanos - frame.delayNanos; - long delayEnd = frame.endNanos; - - // Intersect the delay interval with the query range - long overlapStart = Math.max(delayStart, startNanos); - long overlapEnd = Math.min(delayEnd, endNanos); - - if (overlapEnd > overlapStart) { - totalDelayNanos += (overlapEnd - overlapStart); - } - } - - return totalDelayNanos / 1e9; - } - - private void pruneOldFrames(long currentNanos) { - long cutoff = currentNanos - MAX_FRAME_AGE_NANOS; - // Remove from the front one-by-one. CopyOnWriteArrayList.remove(0) is O(n) per call, - // but old frames are pruned incrementally so typically only 0-1 entries are removed. - while (!frames.isEmpty() && frames.get(0).endNanos < cutoff) { - frames.remove(0); - } - } - - private static class FrameRecord { - final long startNanos; - final long endNanos; - final long delayNanos; - - FrameRecord(long startNanos, long endNanos, long delayNanos) { - this.startNanos = startNanos; - this.endNanos = endNanos; - this.delayNanos = delayNanos; - } - } -} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 4136eb5d3b..c255439416 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -48,6 +48,7 @@ import io.sentry.android.core.SentryShakeDetector; import io.sentry.android.core.ViewHierarchyEventProcessor; import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; +import io.sentry.android.core.SentryFramesDelayResult; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.protocol.Geo; @@ -98,7 +99,8 @@ public class RNSentryModuleImpl { private final ReactApplicationContext reactApplicationContext; private final PackageInfo packageInfo; private FrameMetricsAggregator frameMetricsAggregator = null; - private final RNSentryFrameDelayCollector frameDelayCollector = new RNSentryFrameDelayCollector(); + private @Nullable SentryFrameMetricsCollector frameMetricsCollector = null; + private @Nullable String frameMetricsListenerId = null; private boolean androidXAvailable; @VisibleForTesting static long lastStartTimestampMs = -1; @@ -413,9 +415,15 @@ public void fetchNativeFramesDelay( long startNanos = nowNanos - (long) (startOffsetSeconds * 1e9); long endNanos = nowNanos - (long) (endOffsetSeconds * 1e9); - double delaySeconds = frameDelayCollector.getFramesDelay(startNanos, endNanos); - if (delaySeconds >= 0) { - promise.resolve(delaySeconds); + if (frameMetricsCollector == null) { + promise.resolve(null); + return; + } + + SentryFramesDelayResult result = + frameMetricsCollector.getFramesDelay(startNanos, endNanos); + if (result.getDelaySeconds() >= 0) { + promise.resolve(result.getDelaySeconds()); } else { promise.resolve(null); } @@ -747,12 +755,22 @@ public void enableNativeFramesTracking() { if (options instanceof SentryAndroidOptions) { final SentryFrameMetricsCollector collector = ((SentryAndroidOptions) options).getFrameMetricsCollector(); - if (frameDelayCollector.start(collector)) { - logger.log(SentryLevel.INFO, "RNSentryFrameDelayCollector installed."); + if (collector != null) { + // Register a no-op listener to ensure frame metrics collection is active. + // This is needed so that getFramesDelay() has data to query. + stopFrameMetricsCollection(); + frameMetricsCollector = collector; + frameMetricsListenerId = + collector.startCollection( + (startNanos, endNanos, durationNanos, delayNanos, isSlow, isFrozen, refreshRate) + -> {}); + if (frameMetricsListenerId != null) { + logger.log(SentryLevel.INFO, "SentryFrameMetricsCollector listener installed."); + } } } } catch (Throwable ignored) { // NOPMD - We don't want to crash in any case - logger.log(SentryLevel.WARNING, "Error starting RNSentryFrameDelayCollector."); + logger.log(SentryLevel.WARNING, "Error starting frame metrics collection."); } } @@ -761,7 +779,15 @@ public void disableNativeFramesTracking() { frameMetricsAggregator.stop(); frameMetricsAggregator = null; } - frameDelayCollector.stop(); + stopFrameMetricsCollection(); + } + + private void stopFrameMetricsCollection() { + if (frameMetricsCollector != null && frameMetricsListenerId != null) { + frameMetricsCollector.stopCollection(frameMetricsListenerId); + } + frameMetricsCollector = null; + frameMetricsListenerId = null; } public void getNewScreenTimeToDisplay(Promise promise) { From ea9d0b63e7cb16175bd816cc0c7a6a254f8618ee Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 30 Apr 2026 11:27:39 +0200 Subject: [PATCH 2/3] Add chanfgelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a3dce4d70..1a21c7c977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Escape `name` and `version` values when injecting release constants into the web bundle ([#6044](https://github.com/getsentry/sentry-react-native/pull/6044)) - Mask the Sentry auth token in the `sentry.gradle` upload-task lifecycle log ([#6057](https://github.com/getsentry/sentry-react-native/pull/6057)) - Discard invalid navigation/interaction transactions via an event processor instead of mutating the internal `_sampled` flag, removing misleading "dropped due to sampling" debug logs ([#6051](https://github.com/getsentry/sentry-react-native/pull/6051)) +- Use sentry-java `getFramesDelay` API instead of custom frame delay collector ([#6074](https://github.com/getsentry/sentry-react-native/pull/6074)) ### Dependencies From 3984ffe455c71bedf9db0fcadcd1e6129fcfe934 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 4 May 2026 13:23:14 +0200 Subject: [PATCH 3/3] Update CHANGELOG for sentry-java API usage Updated the changelog to reflect the use of sentry-java's getFramesDelay API and removed duplicate entry. --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b21972a66..6dc2e2964c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Fixes + +- Use sentry-java `getFramesDelay` API instead of custom frame delay collector ([#6074](https://github.com/getsentry/sentry-react-native/pull/6074)) + ## 8.10.0 ### Features @@ -23,7 +29,6 @@ - Escape `name` and `version` values when injecting release constants into the web bundle ([#6044](https://github.com/getsentry/sentry-react-native/pull/6044)) - Mask the Sentry auth token in the `sentry.gradle` upload-task lifecycle log ([#6057](https://github.com/getsentry/sentry-react-native/pull/6057)) - Discard invalid navigation/interaction transactions via an event processor instead of mutating the internal `_sampled` flag, removing misleading "dropped due to sampling" debug logs ([#6051](https://github.com/getsentry/sentry-react-native/pull/6051)) -- Use sentry-java `getFramesDelay` API instead of custom frame delay collector ([#6074](https://github.com/getsentry/sentry-react-native/pull/6074)) ### Dependencies