@@ -152,49 +152,11 @@ private void captureFrameMetrics(@NotNull final ISpan span) {
152152 return ;
153153 }
154154
155- final @ NotNull SentryFrameMetrics frameMetrics = new SentryFrameMetrics ();
156-
157- long frameDurationNanos = lastKnownFrameDurationNanos ;
158-
159- if (!frames .isEmpty ()) {
160- // determine relevant start in frames list
161- final Iterator <Frame > iterator = frames .tailSet (new Frame (spanStartNanos )).iterator ();
162-
163- //noinspection WhileLoopReplaceableByForEach
164- while (iterator .hasNext ()) {
165- final @ NotNull Frame frame = iterator .next ();
166-
167- if (frame .startNanos > spanEndNanos ) {
168- break ;
169- }
170-
171- if (frame .startNanos >= spanStartNanos && frame .endNanos <= spanEndNanos ) {
172- // if the frame is contained within the span, add it 1:1 to the span metrics
173- frameMetrics .addFrame (
174- frame .durationNanos , frame .delayNanos , frame .isSlow , frame .isFrozen );
175- } else if ((spanStartNanos > frame .startNanos && spanStartNanos < frame .endNanos )
176- || (spanEndNanos > frame .startNanos && spanEndNanos < frame .endNanos )) {
177- // span start or end are within frame
178- // calculate the intersection
179- final long durationBeforeSpan = Math .max (0 , spanStartNanos - frame .startNanos );
180- final long delayBeforeSpan =
181- Math .max (0 , durationBeforeSpan - frame .expectedDurationNanos );
182- final long delayWithinSpan =
183- Math .min (frame .delayNanos - delayBeforeSpan , spanDurationNanos );
184-
185- final long frameStart = Math .max (spanStartNanos , frame .startNanos );
186- final long frameEnd = Math .min (spanEndNanos , frame .endNanos );
187- final long frameDuration = frameEnd - frameStart ;
188- frameMetrics .addFrame (
189- frameDuration ,
190- delayWithinSpan ,
191- SentryFrameMetricsCollector .isSlow (frameDuration , frame .expectedDurationNanos ),
192- SentryFrameMetricsCollector .isFrozen (frameDuration ));
193- }
194-
195- frameDurationNanos = frame .expectedDurationNanos ;
196- }
197- }
155+ // effectiveFrameDuration tracks the expected frame duration of the last frame
156+ // iterated within the span's time range, falling back to lastKnownFrameDurationNanos
157+ final long [] effectiveFrameDuration = {lastKnownFrameDurationNanos };
158+ final @ NotNull SentryFrameMetrics frameMetrics =
159+ calculateFrameMetrics (spanStartNanos , spanEndNanos , effectiveFrameDuration );
198160
199161 int totalFrameCount = frameMetrics .getSlowFrozenFrameCount ();
200162
@@ -204,9 +166,9 @@ private void captureFrameMetrics(@NotNull final ISpan span) {
204166 if (nextScheduledFrameNanos != -1 ) {
205167 totalFrameCount +=
206168 addPendingFrameDelay (
207- frameMetrics , frameDurationNanos , spanEndNanos , nextScheduledFrameNanos );
169+ frameMetrics , effectiveFrameDuration [ 0 ] , spanEndNanos , nextScheduledFrameNanos );
208170 totalFrameCount +=
209- interpolateFrameCount (frameMetrics , frameDurationNanos , spanDurationNanos );
171+ interpolateFrameCount (frameMetrics , effectiveFrameDuration [ 0 ] , spanDurationNanos );
210172 }
211173 final long frameDelayNanos =
212174 frameMetrics .getSlowFrameDelayNanos () + frameMetrics .getFrozenFrameDelayNanos ();
@@ -226,6 +188,100 @@ private void captureFrameMetrics(@NotNull final ISpan span) {
226188 }
227189 }
228190
191+ /**
192+ * Queries the frame delay for a given time range, without requiring an active span.
193+ *
194+ * <p>This is useful for external consumers (e.g. React Native SDK) that need to query frame delay
195+ * for an arbitrary time range without registering their own frame listener.
196+ *
197+ * @param startSystemNanos start of the time range in {@link System#nanoTime()} units
198+ * @param endSystemNanos end of the time range in {@link System#nanoTime()} units
199+ * @return a {@link SentryFramesDelayResult} with the delay in seconds and the number of frames
200+ * contributing to delay, or a result with delaySeconds=-1 if incalculable
201+ */
202+ public @ NotNull SentryFramesDelayResult getFramesDelay (
203+ final long startSystemNanos , final long endSystemNanos ) {
204+ if (!enabled ) {
205+ return new SentryFramesDelayResult (-1 , 0 );
206+ }
207+
208+ final long durationNanos = endSystemNanos - startSystemNanos ;
209+ if (durationNanos <= 0 ) {
210+ return new SentryFramesDelayResult (-1 , 0 );
211+ }
212+
213+ final long [] effectiveFrameDuration = {lastKnownFrameDurationNanos };
214+ final @ NotNull SentryFrameMetrics frameMetrics =
215+ calculateFrameMetrics (startSystemNanos , endSystemNanos , effectiveFrameDuration );
216+
217+ final long nextScheduledFrameNanos = frameMetricsCollector .getLastKnownFrameStartTimeNanos ();
218+ if (nextScheduledFrameNanos != -1 ) {
219+ addPendingFrameDelay (
220+ frameMetrics , effectiveFrameDuration [0 ], endSystemNanos , nextScheduledFrameNanos );
221+ }
222+
223+ final long frameDelayNanos =
224+ frameMetrics .getSlowFrameDelayNanos () + frameMetrics .getFrozenFrameDelayNanos ();
225+ final double frameDelayInSeconds = frameDelayNanos / 1e9d ;
226+
227+ return new SentryFramesDelayResult (frameDelayInSeconds , frameMetrics .getSlowFrozenFrameCount ());
228+ }
229+
230+ /**
231+ * Calculates frame metrics for a given time range by iterating over stored frames and handling
232+ * partial overlaps at the boundaries.
233+ *
234+ * @param startNanos start of the time range
235+ * @param endNanos end of the time range
236+ * @param effectiveFrameDuration a single-element array that will be updated with the expected
237+ * frame duration of the last iterated frame (used for pending delay / interpolation)
238+ */
239+ private @ NotNull SentryFrameMetrics calculateFrameMetrics (
240+ final long startNanos , final long endNanos , final long @ NotNull [] effectiveFrameDuration ) {
241+ final long durationNanos = endNanos - startNanos ;
242+ final @ NotNull SentryFrameMetrics frameMetrics = new SentryFrameMetrics ();
243+
244+ if (!frames .isEmpty ()) {
245+ final Iterator <Frame > iterator = frames .tailSet (new Frame (startNanos )).iterator ();
246+
247+ //noinspection WhileLoopReplaceableByForEach
248+ while (iterator .hasNext ()) {
249+ final @ NotNull Frame frame = iterator .next ();
250+
251+ if (frame .startNanos > endNanos ) {
252+ break ;
253+ }
254+
255+ if (frame .startNanos >= startNanos && frame .endNanos <= endNanos ) {
256+ // if the frame is contained within the range, add it 1:1
257+ frameMetrics .addFrame (
258+ frame .durationNanos , frame .delayNanos , frame .isSlow , frame .isFrozen );
259+ } else if ((startNanos > frame .startNanos && startNanos < frame .endNanos )
260+ || (endNanos > frame .startNanos && endNanos < frame .endNanos )) {
261+ // range start or end are within frame — calculate the intersection
262+ final long durationBeforeRange = Math .max (0 , startNanos - frame .startNanos );
263+ final long delayBeforeRange =
264+ Math .max (0 , durationBeforeRange - frame .expectedDurationNanos );
265+ final long delayWithinRange =
266+ Math .min (frame .delayNanos - delayBeforeRange , durationNanos );
267+
268+ final long frameStart = Math .max (startNanos , frame .startNanos );
269+ final long frameEnd = Math .min (endNanos , frame .endNanos );
270+ final long frameDuration = frameEnd - frameStart ;
271+ frameMetrics .addFrame (
272+ frameDuration ,
273+ delayWithinRange ,
274+ SentryFrameMetricsCollector .isSlow (frameDuration , frame .expectedDurationNanos ),
275+ SentryFrameMetricsCollector .isFrozen (frameDuration ));
276+ }
277+
278+ effectiveFrameDuration [0 ] = frame .expectedDurationNanos ;
279+ }
280+ }
281+
282+ return frameMetrics ;
283+ }
284+
229285 @ Override
230286 public void clear () {
231287 try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
0 commit comments