diff --git a/flutter/lib/src/span_frame_metrics_collector.dart b/flutter/lib/src/span_frame_metrics_collector.dart index 25f257237..511f04fa8 100644 --- a/flutter/lib/src/span_frame_metrics_collector.dart +++ b/flutter/lib/src/span_frame_metrics_collector.dart @@ -23,11 +23,11 @@ class SpanFrameMetricsCollector implements PerformanceContinuousCollector { final bool _isTestMode; - /// Stores frame timestamps and their durations in milliseconds. + /// Stores timestamps and durations (in milliseconds) of frames exceeding the expected duration. /// Keys are frame timestamps, values are frame durations. /// The timestamps mark the end of the frame. @visibleForTesting - final frames = SplayTreeMap(); + final exceededFrames = SplayTreeMap(); /// Stores the spans that are actively being tracked. /// After the frames are calculated and stored in the span the span is removed from this list. @@ -103,7 +103,7 @@ class SpanFrameMetricsCollector implements PerformanceContinuousCollector { if (activeSpans.isEmpty) { clear(); } else { - frames.removeWhere((frameTimestamp, _) => + exceededFrames.removeWhere((frameTimestamp, _) => frameTimestamp.isBefore(activeSpans.first.startTimestamp)); } } @@ -122,21 +122,27 @@ class SpanFrameMetricsCollector implements PerformanceContinuousCollector { } } - /// Records the duration of a single frame and stores it in [frames]. + /// Records the duration of a single frame and stores it in [exceededFrames]. /// /// This method is called for each frame when frame tracking is active. Future measureFrameDuration(Duration duration) async { - if (frames.length >= maxFramesToTrack) { + if (exceededFrames.length >= maxFramesToTrack) { options.logger(SentryLevel.warning, 'Frame tracking limit reached. Clearing frames and cancelling frame tracking for all active spans'); clear(); return; } + if (expectedFrameDuration == null) { + options.logger(SentryLevel.info, + 'Expected frame duration is null. Dropping frame duration.'); + return; + } + // Using the stopwatch to measure the frame duration is flaky in ci if (_isTestMode) { // ignore: invalid_use_of_internal_member - frames[options.clock().add(duration)] = duration.inMilliseconds; + exceededFrames[options.clock().add(duration)] = duration.inMilliseconds; return; } @@ -149,9 +155,9 @@ class SpanFrameMetricsCollector implements PerformanceContinuousCollector { await _frameCallbackHandler.endOfFrame; final frameDuration = _stopwatch.elapsedMilliseconds; - if (_isFrozenFrame(frameDuration) || _isSlowFrame(frameDuration)) { + if (frameDuration > expectedFrameDuration!.inMilliseconds) { // ignore: invalid_use_of_internal_member - frames[options.clock()] = frameDuration; + exceededFrames[options.clock()] = frameDuration; } _stopwatch.reset(); @@ -184,7 +190,7 @@ class SpanFrameMetricsCollector implements PerformanceContinuousCollector { @visibleForTesting Map calculateFrameMetrics( ISentrySpan span, DateTime spanEndTimestamp, int displayRefreshRate) { - if (frames.isEmpty) { + if (exceededFrames.isEmpty) { options.logger( SentryLevel.info, 'No frame durations available in frame tracker.'); return {}; @@ -202,7 +208,7 @@ class SpanFrameMetricsCollector implements PerformanceContinuousCollector { int frozenFramesDuration = 0; int framesDelay = 0; - for (final entry in frames.entries) { + for (final entry in exceededFrames.entries) { final frameDuration = entry.value; final frameEndTimestamp = entry.key; final frameStartMs = @@ -241,10 +247,10 @@ class SpanFrameMetricsCollector implements PerformanceContinuousCollector { break; } - if (_isFrozenFrame(effectiveDuration)) { + if (effectiveDuration >= _frozenFrameThreshold.inMilliseconds) { frozenFramesCount++; frozenFramesDuration += effectiveDuration; - } else if (_isSlowFrame(effectiveDuration)) { + } else if (effectiveDuration > expectedFrameDuration!.inMilliseconds) { slowFramesCount++; slowFramesDuration += effectiveDuration; } @@ -284,23 +290,11 @@ class SpanFrameMetricsCollector implements PerformanceContinuousCollector { }; } - bool _isFrozenFrame(int frameDuration) { - return frameDuration > _frozenFrameThreshold.inMilliseconds; - } - - bool _isSlowFrame(int frameDuration) { - if (expectedFrameDuration != null) { - return frameDuration > expectedFrameDuration!.inMilliseconds; - } else { - return false; - } - } - @override void clear() { _isTrackingPaused = true; _stopwatch.reset(); - frames.clear(); + exceededFrames.clear(); activeSpans.clear(); displayRefreshRate = null; } diff --git a/flutter/test/span_frame_metrics_collector_test.dart b/flutter/test/span_frame_metrics_collector_test.dart index c6addb7ae..31f6eab82 100644 --- a/flutter/test/span_frame_metrics_collector_test.dart +++ b/flutter/test/span_frame_metrics_collector_test.dart @@ -25,14 +25,14 @@ void main() { test('clear() clears frames, running spans and pauses frame tracking', () { final sut = fixture.sut; - sut.frames[DateTime.now()] = 1; + sut.exceededFrames[DateTime.now()] = 1; final mockSpan = MockSentrySpan(); when(mockSpan.startTimestamp).thenReturn(DateTime.now()); sut.onSpanStarted(mockSpan); sut.clear(); - expect(sut.frames, isEmpty); + expect(sut.exceededFrames, isEmpty); expect(sut.activeSpans, isEmpty); expect(sut.isTrackingPaused, isTrue); }); @@ -97,14 +97,15 @@ void main() { sut.activeSpans.add(span1); sut.activeSpans.add(span2); - sut.frames[spanStartTimestamp.subtract(Duration(seconds: 5))] = 1; - sut.frames[spanStartTimestamp.subtract(Duration(seconds: 3))] = 1; - sut.frames[spanStartTimestamp.add(Duration(seconds: 4))] = 1; + sut.exceededFrames[spanStartTimestamp.subtract(Duration(seconds: 5))] = 1; + sut.exceededFrames[spanStartTimestamp.subtract(Duration(seconds: 3))] = 1; + sut.exceededFrames[spanStartTimestamp.add(Duration(seconds: 4))] = 1; await sut.onSpanFinished(span1, spanEndTimestamp); - expect(sut.frames, hasLength(1)); - expect(sut.frames.keys.first, spanStartTimestamp.add(Duration(seconds: 4))); + expect(sut.exceededFrames, hasLength(1)); + expect(sut.exceededFrames.keys.first, + spanStartTimestamp.add(Duration(seconds: 4))); }); test( @@ -141,7 +142,7 @@ void main() { final now = DateTime.now(); when(span.startTimestamp).thenReturn(now); when(span.endTimestamp).thenReturn(now.add(Duration(milliseconds: 500))); - sut.frames[now.add(Duration(milliseconds: 200))] = 100; + sut.exceededFrames[now.add(Duration(milliseconds: 200))] = 100; final metrics = sut.calculateFrameMetrics(span, span.endTimestamp!, 60); @@ -159,7 +160,7 @@ void main() { final now = DateTime.now(); when(span.startTimestamp).thenReturn(now); when(span.endTimestamp).thenReturn(now.add(Duration(milliseconds: 500))); - sut.frames[now.subtract(Duration(milliseconds: 200))] = 100; + sut.exceededFrames[now.subtract(Duration(milliseconds: 200))] = 100; final metrics = sut.calculateFrameMetrics(span, span.endTimestamp!, 60); @@ -179,7 +180,7 @@ void main() { when(span.startTimestamp).thenReturn(now); when(span.endTimestamp).thenReturn(now.add(Duration(milliseconds: 500))); // 50ms before span starts and ends 50ms after span starts - sut.frames[now.add(Duration(milliseconds: 50))] = 100; + sut.exceededFrames[now.add(Duration(milliseconds: 50))] = 100; final metrics = sut.calculateFrameMetrics(span, span.endTimestamp!, 60); @@ -198,7 +199,7 @@ void main() { final now = DateTime.now(); when(span.startTimestamp).thenReturn(now); when(span.endTimestamp).thenReturn(now.add(Duration(milliseconds: 500))); - sut.frames[now.add(Duration(milliseconds: 550))] = 100; + sut.exceededFrames[now.add(Duration(milliseconds: 550))] = 100; final metrics = sut.calculateFrameMetrics(span, span.endTimestamp!, 60); @@ -269,7 +270,7 @@ void main() { when(span.startTimestamp).thenReturn(DateTime.now()); sut.activeSpans.add(span); - sut.frames[DateTime.now()] = 1; + sut.exceededFrames[DateTime.now()] = 1; const maxFramesToTrack = 1000; sut.maxFramesToTrack = maxFramesToTrack; @@ -277,12 +278,12 @@ void main() { await Future.delayed( Duration(milliseconds: 1)); // Add a small delay if (i == maxFramesToTrack - 1) { - expect(sut.frames.length, maxFramesToTrack - 1); + expect(sut.exceededFrames.length, maxFramesToTrack - 1); } await sut.measureFrameDuration(Duration.zero); } - expect(sut.frames, isEmpty); + expect(sut.exceededFrames, isEmpty); expect(sut.activeSpans, isEmpty); expect(sut.displayRefreshRate, isNull); expect(sut.isTrackingPaused, isTrue);