From d748e5648333c23b860319a72943d449295af2b0 Mon Sep 17 00:00:00 2001 From: Jesus Rojas Date: Mon, 27 Oct 2025 19:15:23 -0600 Subject: [PATCH 1/6] DRAFT: Fix frame rate metrics for dynamic refresh rates #10220 --- .../FPRScreenTraceTracker+Private.h | 10 ---- .../AppActivity/FPRScreenTraceTracker.m | 55 ++++++++++++++++--- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker+Private.h b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker+Private.h index 9cbb868d799..f13100cd340 100644 --- a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker+Private.h +++ b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker+Private.h @@ -37,16 +37,6 @@ FOUNDATION_EXTERN NSString *const kFPRSlowFrameCounterName; /** Counter name for total frames. */ FOUNDATION_EXTERN NSString *const kFPRTotalFramesCounterName; -/** Slow frame threshold (for time difference between current and previous frame render time) - * in sec. - */ -FOUNDATION_EXTERN CFTimeInterval const kFPRSlowFrameThreshold; - -/** Frozen frame threshold (for time difference between current and previous frame render time) - * in sec. - */ -FOUNDATION_EXTERN CFTimeInterval const kFPRFrozenFrameThreshold; - @interface FPRScreenTraceTracker () /** A map table of that has the viewControllers as the keys and their associated trace as the value. diff --git a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m index 5137776fb5f..bed34455897 100644 --- a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m +++ b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m @@ -25,11 +25,21 @@ NSString *const kFPRSlowFrameCounterName = @"_fr_slo"; NSString *const kFPRTotalFramesCounterName = @"_fr_tot"; -// Note: This was previously 60 FPS, but that resulted in 90% + of all frames collected to be -// flagged as slow frames, and so the threshold for iOS is being changed to 59 FPS. -// TODO(b/73498642): Make these configurable. -CFTimeInterval const kFPRSlowFrameThreshold = 1.0 / 59.0; // Anything less than 59 FPS is slow. -CFTimeInterval const kFPRFrozenFrameThreshold = 700.0 / 1000.0; +/** Frozen frame multiplier: A frozen frame is one that takes longer than approximately 42 times + * the current frame duration. This maintains backward compatibility with the old 700ms threshold + * at 60Hz (700ms ÷ 16.67ms ≈ 42 frames). + * + * NOTE!!!: A "frozen" frame represents missing 42 consecutive frame opportunities, + * which looks and feels equally bad to users regardless of refresh rate. + * + * Formula: frozenThreshold = kFPRFrozenFrameMultiplier * actualFrameDuration + * + * Examples (all represent missing 42 frame opportunities): + * - 60Hz: 42 × 16.67ms = 700ms (same as original threshold) + * - 120Hz: 42 × 8.33ms = 350ms (missing 42 frames at higher refresh rate) + * - 50Hz: 42 × 20ms = 840ms (missing 42 frames at lower refresh rate) + */ +static const CFTimeInterval kFPRFrozenFrameMultiplier = 42.0; /** Constant that indicates an invalid time. */ CFAbsoluteTime const kFPRInvalidTime = -1.0; @@ -80,6 +90,9 @@ @implementation FPRScreenTraceTracker { /** Instance variable storing the frozen frames observed so far. */ atomic_int_fast64_t _frozenFramesCount; + + /** Instance variable storing the current frame duration in seconds. */ + _Atomic(CFTimeInterval) _currentFrameDuration; } @dynamic totalFramesCount; @@ -112,6 +125,7 @@ - (instancetype)init { atomic_store_explicit(&_totalFramesCount, 0, memory_order_relaxed); atomic_store_explicit(&_frozenFramesCount, 0, memory_order_relaxed); atomic_store_explicit(&_slowFramesCount, 0, memory_order_relaxed); + atomic_store_explicit(&_currentFrameDuration, 1.0 / 60.0, memory_order_relaxed); // Default fallback to 60Hz _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkStep)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; @@ -142,6 +156,9 @@ - (void)dealloc { } - (void)appDidBecomeActiveNotification:(NSNotification *)notification { + // Resume the display link when the app becomes active + _displayLink.paused = NO; + // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as // soon as we're notified of an event. int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed); @@ -160,6 +177,9 @@ - (void)appDidBecomeActiveNotification:(NSNotification *)notification { } - (void)appWillResignActiveNotification:(NSNotification *)notification { + // Pause the display link when the app goes to background to conserve battery + _displayLink.paused = YES; + // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as // soon as we're notified of an event. int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed); @@ -188,7 +208,18 @@ - (void)appWillResignActiveNotification:(NSNotification *)notification { - (void)displayLinkStep { static CFAbsoluteTime previousTimestamp = kFPRInvalidTime; CFAbsoluteTime currentTimestamp = self.displayLink.timestamp; - RecordFrameType(currentTimestamp, previousTimestamp, &_slowFramesCount, &_frozenFramesCount, + + // Calculate the current frame duration using targetTimestamp and timestamp + // This gives us the actual refresh rate of the display + CFTimeInterval actualFrameDuration = self.displayLink.targetTimestamp - self.displayLink.timestamp; + + // Update the frame duration for use by the frame classification logic + // Only update if we have a valid duration (> 0) to avoid issues with the first frame + if (actualFrameDuration > 0) { + atomic_store_explicit(&_currentFrameDuration, actualFrameDuration, memory_order_relaxed); + } + + RecordFrameType(currentTimestamp, previousTimestamp, actualFrameDuration, &_slowFramesCount, &_frozenFramesCount, &_totalFramesCount); previousTimestamp = currentTimestamp; } @@ -198,6 +229,7 @@ - (void)displayLinkStep { * * @param currentTimestamp The current timestamp of the displayLink. * @param previousTimestamp The previous timestamp of the displayLink. + * @param actualFrameDuration The actual frame duration calculated from CADisplayLink's targetTimestamp and timestamp. * @param slowFramesCounter The value of the slowFramesCount before this function was called. * @param frozenFramesCounter The value of the frozenFramesCount before this function was called. * @param totalFramesCounter The value of the totalFramesCount before this function was called. @@ -205,19 +237,24 @@ - (void)displayLinkStep { FOUNDATION_STATIC_INLINE void RecordFrameType(CFAbsoluteTime currentTimestamp, CFAbsoluteTime previousTimestamp, + CFTimeInterval actualFrameDuration, atomic_int_fast64_t *slowFramesCounter, atomic_int_fast64_t *frozenFramesCounter, atomic_int_fast64_t *totalFramesCounter) { CFTimeInterval frameDuration = currentTimestamp - previousTimestamp; - if (previousTimestamp == kFPRInvalidTime) { + if (previousTimestamp == kFPRInvalidTime || actualFrameDuration <= 0) { return; } - if (frameDuration > kFPRSlowFrameThreshold) { + + if (frameDuration > actualFrameDuration) { atomic_fetch_add_explicit(slowFramesCounter, 1, memory_order_relaxed); } - if (frameDuration > kFPRFrozenFrameThreshold) { + + CFTimeInterval frozenThreshold = kFPRFrozenFrameMultiplier * actualFrameDuration; + if (frameDuration > frozenThreshold) { atomic_fetch_add_explicit(frozenFramesCounter, 1, memory_order_relaxed); } + atomic_fetch_add_explicit(totalFramesCounter, 1, memory_order_relaxed); } From af084f2bf859e0ee288e0c63081fe1d9876a7e95 Mon Sep 17 00:00:00 2001 From: Jesus Rojas Date: Mon, 27 Oct 2025 19:16:47 -0600 Subject: [PATCH 2/6] Run Style.sh --- .../AppActivity/FPRScreenTraceTracker.m | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m index bed34455897..a6bc69d6a72 100644 --- a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m +++ b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m @@ -28,15 +28,15 @@ /** Frozen frame multiplier: A frozen frame is one that takes longer than approximately 42 times * the current frame duration. This maintains backward compatibility with the old 700ms threshold * at 60Hz (700ms ÷ 16.67ms ≈ 42 frames). - * + * * NOTE!!!: A "frozen" frame represents missing 42 consecutive frame opportunities, * which looks and feels equally bad to users regardless of refresh rate. * * Formula: frozenThreshold = kFPRFrozenFrameMultiplier * actualFrameDuration - * + * * Examples (all represent missing 42 frame opportunities): * - 60Hz: 42 × 16.67ms = 700ms (same as original threshold) - * - 120Hz: 42 × 8.33ms = 350ms (missing 42 frames at higher refresh rate) + * - 120Hz: 42 × 8.33ms = 350ms (missing 42 frames at higher refresh rate) * - 50Hz: 42 × 20ms = 840ms (missing 42 frames at lower refresh rate) */ static const CFTimeInterval kFPRFrozenFrameMultiplier = 42.0; @@ -125,7 +125,8 @@ - (instancetype)init { atomic_store_explicit(&_totalFramesCount, 0, memory_order_relaxed); atomic_store_explicit(&_frozenFramesCount, 0, memory_order_relaxed); atomic_store_explicit(&_slowFramesCount, 0, memory_order_relaxed); - atomic_store_explicit(&_currentFrameDuration, 1.0 / 60.0, memory_order_relaxed); // Default fallback to 60Hz + atomic_store_explicit(&_currentFrameDuration, 1.0 / 60.0, + memory_order_relaxed); // Default fallback to 60Hz _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkStep)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; @@ -158,7 +159,7 @@ - (void)dealloc { - (void)appDidBecomeActiveNotification:(NSNotification *)notification { // Resume the display link when the app becomes active _displayLink.paused = NO; - + // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as // soon as we're notified of an event. int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed); @@ -179,7 +180,7 @@ - (void)appDidBecomeActiveNotification:(NSNotification *)notification { - (void)appWillResignActiveNotification:(NSNotification *)notification { // Pause the display link when the app goes to background to conserve battery _displayLink.paused = YES; - + // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as // soon as we're notified of an event. int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed); @@ -208,19 +209,20 @@ - (void)appWillResignActiveNotification:(NSNotification *)notification { - (void)displayLinkStep { static CFAbsoluteTime previousTimestamp = kFPRInvalidTime; CFAbsoluteTime currentTimestamp = self.displayLink.timestamp; - + // Calculate the current frame duration using targetTimestamp and timestamp // This gives us the actual refresh rate of the display - CFTimeInterval actualFrameDuration = self.displayLink.targetTimestamp - self.displayLink.timestamp; - + CFTimeInterval actualFrameDuration = + self.displayLink.targetTimestamp - self.displayLink.timestamp; + // Update the frame duration for use by the frame classification logic // Only update if we have a valid duration (> 0) to avoid issues with the first frame if (actualFrameDuration > 0) { atomic_store_explicit(&_currentFrameDuration, actualFrameDuration, memory_order_relaxed); } - - RecordFrameType(currentTimestamp, previousTimestamp, actualFrameDuration, &_slowFramesCount, &_frozenFramesCount, - &_totalFramesCount); + + RecordFrameType(currentTimestamp, previousTimestamp, actualFrameDuration, &_slowFramesCount, + &_frozenFramesCount, &_totalFramesCount); previousTimestamp = currentTimestamp; } @@ -229,7 +231,8 @@ - (void)displayLinkStep { * * @param currentTimestamp The current timestamp of the displayLink. * @param previousTimestamp The previous timestamp of the displayLink. - * @param actualFrameDuration The actual frame duration calculated from CADisplayLink's targetTimestamp and timestamp. + * @param actualFrameDuration The actual frame duration calculated from CADisplayLink's + * targetTimestamp and timestamp. * @param slowFramesCounter The value of the slowFramesCount before this function was called. * @param frozenFramesCounter The value of the frozenFramesCount before this function was called. * @param totalFramesCounter The value of the totalFramesCount before this function was called. @@ -245,16 +248,16 @@ void RecordFrameType(CFAbsoluteTime currentTimestamp, if (previousTimestamp == kFPRInvalidTime || actualFrameDuration <= 0) { return; } - + if (frameDuration > actualFrameDuration) { atomic_fetch_add_explicit(slowFramesCounter, 1, memory_order_relaxed); } - + CFTimeInterval frozenThreshold = kFPRFrozenFrameMultiplier * actualFrameDuration; if (frameDuration > frozenThreshold) { atomic_fetch_add_explicit(frozenFramesCounter, 1, memory_order_relaxed); } - + atomic_fetch_add_explicit(totalFramesCounter, 1, memory_order_relaxed); } From b2962c7a6dc5000dce7d3d09dcd962be9d13d8f5 Mon Sep 17 00:00:00 2001 From: Jesus Rojas Date: Mon, 27 Oct 2025 20:22:41 -0600 Subject: [PATCH 3/6] Gemini Code Review Suggestions --- .../Sources/AppActivity/FPRScreenTraceTracker.m | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m index a6bc69d6a72..712d58531b6 100644 --- a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m +++ b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m @@ -90,9 +90,6 @@ @implementation FPRScreenTraceTracker { /** Instance variable storing the frozen frames observed so far. */ atomic_int_fast64_t _frozenFramesCount; - - /** Instance variable storing the current frame duration in seconds. */ - _Atomic(CFTimeInterval) _currentFrameDuration; } @dynamic totalFramesCount; @@ -125,8 +122,6 @@ - (instancetype)init { atomic_store_explicit(&_totalFramesCount, 0, memory_order_relaxed); atomic_store_explicit(&_frozenFramesCount, 0, memory_order_relaxed); atomic_store_explicit(&_slowFramesCount, 0, memory_order_relaxed); - atomic_store_explicit(&_currentFrameDuration, 1.0 / 60.0, - memory_order_relaxed); // Default fallback to 60Hz _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkStep)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; @@ -215,12 +210,6 @@ - (void)displayLinkStep { CFTimeInterval actualFrameDuration = self.displayLink.targetTimestamp - self.displayLink.timestamp; - // Update the frame duration for use by the frame classification logic - // Only update if we have a valid duration (> 0) to avoid issues with the first frame - if (actualFrameDuration > 0) { - atomic_store_explicit(&_currentFrameDuration, actualFrameDuration, memory_order_relaxed); - } - RecordFrameType(currentTimestamp, previousTimestamp, actualFrameDuration, &_slowFramesCount, &_frozenFramesCount, &_totalFramesCount); previousTimestamp = currentTimestamp; From 54c76df3f47c13a359431443c2d938685d7a9376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Rojas?= Date: Tue, 28 Oct 2025 13:08:37 -0600 Subject: [PATCH 4/6] Update FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m index 712d58531b6..686dc0e611c 100644 --- a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m +++ b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m @@ -29,7 +29,7 @@ * the current frame duration. This maintains backward compatibility with the old 700ms threshold * at 60Hz (700ms ÷ 16.67ms ≈ 42 frames). * - * NOTE!!!: A "frozen" frame represents missing 42 consecutive frame opportunities, + * Note: A "frozen" frame represents missing 42 consecutive frame opportunities, * which looks and feels equally bad to users regardless of refresh rate. * * Formula: frozenThreshold = kFPRFrozenFrameMultiplier * actualFrameDuration From 660121790bb3d10c8ad0ea95af580e6fa98965c8 Mon Sep 17 00:00:00 2001 From: Jesus Rojas Date: Wed, 29 Oct 2025 12:39:41 -0600 Subject: [PATCH 5/6] VRR Edge Cases --- .../AppActivity/FPRScreenTraceTracker.m | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m index 686dc0e611c..9b67b18e086 100644 --- a/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m +++ b/FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m @@ -90,6 +90,8 @@ @implementation FPRScreenTraceTracker { /** Instance variable storing the frozen frames observed so far. */ atomic_int_fast64_t _frozenFramesCount; + + CFAbsoluteTime _previousTimestamp; } @dynamic totalFramesCount; @@ -124,6 +126,7 @@ - (instancetype)init { atomic_store_explicit(&_slowFramesCount, 0, memory_order_relaxed); _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkStep)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + _previousTimestamp = kFPRInvalidTime; // We don't receive background and foreground events from analytics and so we have to listen to // them ourselves. @@ -154,6 +157,7 @@ - (void)dealloc { - (void)appDidBecomeActiveNotification:(NSNotification *)notification { // Resume the display link when the app becomes active _displayLink.paused = NO; + _previousTimestamp = kFPRInvalidTime; // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as // soon as we're notified of an event. @@ -175,6 +179,7 @@ - (void)appDidBecomeActiveNotification:(NSNotification *)notification { - (void)appWillResignActiveNotification:(NSNotification *)notification { // Pause the display link when the app goes to background to conserve battery _displayLink.paused = YES; + _previousTimestamp = kFPRInvalidTime; // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as // soon as we're notified of an event. @@ -202,17 +207,23 @@ - (void)appWillResignActiveNotification:(NSNotification *)notification { #pragma mark - Frozen, slow and good frames - (void)displayLinkStep { - static CFAbsoluteTime previousTimestamp = kFPRInvalidTime; CFAbsoluteTime currentTimestamp = self.displayLink.timestamp; - // Calculate the current frame duration using targetTimestamp and timestamp - // This gives us the actual refresh rate of the display CFTimeInterval actualFrameDuration = self.displayLink.targetTimestamp - self.displayLink.timestamp; - RecordFrameType(currentTimestamp, previousTimestamp, actualFrameDuration, &_slowFramesCount, + // Defensive: skip classification when frame budget is zero/negative (e.g., lifecycle/VRR edges) + if (actualFrameDuration <= 0) { + _previousTimestamp = currentTimestamp; + return; + } + + // Dynamic thresholds: slow if frameDuration > frameBudget; frozen if frameDuration > 42 * + // frameBudget. frameBudget is derived from CADisplayLink targetTimestamp - timestamp and adapts + // to VRR/50/60/120 Hz. + RecordFrameType(currentTimestamp, _previousTimestamp, actualFrameDuration, &_slowFramesCount, &_frozenFramesCount, &_totalFramesCount); - previousTimestamp = currentTimestamp; + _previousTimestamp = currentTimestamp; } /** This function increments the relevant frame counters based on the current and previous @@ -234,10 +245,12 @@ void RecordFrameType(CFAbsoluteTime currentTimestamp, atomic_int_fast64_t *frozenFramesCounter, atomic_int_fast64_t *totalFramesCounter) { CFTimeInterval frameDuration = currentTimestamp - previousTimestamp; - if (previousTimestamp == kFPRInvalidTime || actualFrameDuration <= 0) { + if (previousTimestamp == kFPRInvalidTime || actualFrameDuration <= 0 || frameDuration <= 0) { return; } + // Dynamic thresholds: classify against the runtime-derived frame budget. + // Slow: frameDuration > actualFrameDuration; Frozen: frameDuration > (42 * actualFrameDuration). if (frameDuration > actualFrameDuration) { atomic_fetch_add_explicit(slowFramesCounter, 1, memory_order_relaxed); } From ca1ca2828f2fcdf6c292e5e3fa9d3dc5203c11f7 Mon Sep 17 00:00:00 2001 From: Jesus Rojas Date: Wed, 29 Oct 2025 17:25:45 -0600 Subject: [PATCH 6/6] Make Unit Tests Conform to CADisplayLink Implementation --- .../Tests/Unit/FPRScreenTraceTrackerTest.m | 90 +++++++++++++++---- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/FirebasePerformance/Tests/Unit/FPRScreenTraceTrackerTest.m b/FirebasePerformance/Tests/Unit/FPRScreenTraceTrackerTest.m index 2f5cbf40c61..36d639bad53 100644 --- a/FirebasePerformance/Tests/Unit/FPRScreenTraceTrackerTest.m +++ b/FirebasePerformance/Tests/Unit/FPRScreenTraceTrackerTest.m @@ -23,6 +23,13 @@ #import #import "FirebasePerformance/Tests/Unit/FPRTestCase.h" +static inline CFTimeInterval FPRFrameBudgetForHz(double hz) { + return 1.0 / hz; +} +static inline CFTimeInterval FPRFrozenThresholdForBudget(CFTimeInterval budget) { + return 42.0 * budget; +} + /** Registers and returns an instance of a custom subclass of UIViewController. */ static UIViewController *FPRCustomViewController(NSString *className, BOOL isViewLoaded) { Class customClass = NSClassFromString(className); @@ -603,20 +610,38 @@ - (void)testAppDidBecomeActiveWillNotRestoreTracesOfNilledViewControllers { * slow frame counter of the screen trace tracker is incremented. */ - (void)testSlowFrameIsRecorded { + CFTimeInterval frameBudget = FPRFrameBudgetForHz(60.0); CFAbsoluteTime firstFrameRenderTimestamp = 1.0; - CFAbsoluteTime secondFrameRenderTimestamp = - firstFrameRenderTimestamp + kFPRSlowFrameThreshold + 0.005; // Buffer for float comparison. + CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + frameBudget + 0.005; id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; - // Set/Reset the previousFrameTimestamp if it has been set by a previous test. - OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); + // Drive CADisplayLink with matched timestamp/targetTimestamp pairs per tick. + __block NSInteger tick = 0; + NSArray *timestamps = + @[ @(firstFrameRenderTimestamp), @(secondFrameRenderTimestamp) ]; + NSArray *targets = + @[ @(firstFrameRenderTimestamp + frameBudget), @(secondFrameRenderTimestamp + frameBudget) ]; + + OCMStub([displayLinkMock isPaused]).andReturn(NO); + + OCMStub([displayLinkMock timestamp]).andDo(^(NSInvocation *inv) { + CFTimeInterval v = timestamps[(NSUInteger)tick].doubleValue; + [inv setReturnValue:&v]; + }); + OCMStub([displayLinkMock targetTimestamp]).andDo(^(NSInvocation *inv) { + CFTimeInterval v = targets[(NSUInteger)tick].doubleValue; + [inv setReturnValue:&v]; + }); + + // Tick 1 - prime previous timestamp (no counts expected). [self.tracker displayLinkStep]; int64_t initialSlowFramesCount = self.tracker.slowFramesCount; - OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp); + // Tick 2 - slow frame: duration > budget. + tick++; [self.tracker displayLinkStep]; int64_t newSlowFramesCount = self.tracker.slowFramesCount; @@ -625,13 +650,16 @@ - (void)testSlowFrameIsRecorded { /** Tests that the slow and frozen frame counter is not incremented in the case of a good frame. */ - (void)testSlowAndFrozenFrameIsNotRecordedInCaseOfGoodFrame { + CFTimeInterval frameBudget = FPRFrameBudgetForHz(60.0); CFAbsoluteTime firstFrameRenderTimestamp = 1.0; - CFAbsoluteTime secondFrameRenderTimestamp = - firstFrameRenderTimestamp + kFPRSlowFrameThreshold - 0.005; // Good frame. + CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + frameBudget - 0.005; id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; + OCMStub([displayLinkMock targetTimestamp]) + .andReturn(firstFrameRenderTimestamp + frameBudget) + .andReturn(secondFrameRenderTimestamp + frameBudget); // Set/Reset the previousFrameTimestamp if it has been set by a previous test. OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); @@ -651,13 +679,16 @@ - (void)testSlowAndFrozenFrameIsNotRecordedInCaseOfGoodFrame { /* Tests that the frozen frame counter is not incremented in case of a slow frame. */ - (void)testFrozenFrameIsNotRecordedInCaseOfSlowFrame { + CFTimeInterval frameBudget = FPRFrameBudgetForHz(60.0); CFAbsoluteTime firstFrameRenderTimestamp = 1.0; - CFAbsoluteTime secondFrameRenderTimestamp = - firstFrameRenderTimestamp + kFPRSlowFrameThreshold + 0.005; // Slow frame. + CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + frameBudget + 0.005; id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; + OCMStub([displayLinkMock targetTimestamp]) + .andReturn(firstFrameRenderTimestamp + frameBudget) + .andReturn(secondFrameRenderTimestamp + frameBudget); // Set/Reset the previousFrameTimestamp if it has been set by a previous test. OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); @@ -675,17 +706,24 @@ - (void)testFrozenFrameIsNotRecordedInCaseOfSlowFrame { * frames. */ - (void)testTotalFramesAreAlwaysRecorded { + CFTimeInterval frameBudget = FPRFrameBudgetForHz(60.0); + CFTimeInterval frozenThreshold = FPRFrozenThresholdForBudget(frameBudget); CFAbsoluteTime firstFrameRenderTimestamp = 1.0; CFAbsoluteTime secondFrameRenderTimestamp = - firstFrameRenderTimestamp + kFPRSlowFrameThreshold - 0.005; // Good frame. + firstFrameRenderTimestamp + frameBudget - 0.005; // Good frame. CFAbsoluteTime thirdFrameRenderTimestamp = - secondFrameRenderTimestamp + kFPRSlowFrameThreshold + 0.005; // Slow frame. + secondFrameRenderTimestamp + frameBudget + 0.005; // Slow frame. CFAbsoluteTime fourthFrameRenderTimestamp = - thirdFrameRenderTimestamp + kFPRFrozenFrameThreshold + 0.005; // Frozen frame. + thirdFrameRenderTimestamp + frozenThreshold + 0.005; // Frozen frame. id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; + OCMStub([displayLinkMock targetTimestamp]) + .andReturn(firstFrameRenderTimestamp + frameBudget) + .andReturn(secondFrameRenderTimestamp + frameBudget) + .andReturn(thirdFrameRenderTimestamp + frameBudget) + .andReturn(fourthFrameRenderTimestamp + frameBudget); // Set/Reset the previousFrameTimestamp if it has been set by a previous test. OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); @@ -712,21 +750,41 @@ - (void)testTotalFramesAreAlwaysRecorded { * frozen frame counter and slow frame counter of the screen trace tracker is incremented. */ - (void)testFrozenFrameAndSlowFrameIsRecorded { + CFTimeInterval frameBudget = FPRFrameBudgetForHz(60.0); + CFTimeInterval frozenThreshold = FPRFrozenThresholdForBudget(frameBudget); CFAbsoluteTime firstFrameRenderTimestamp = 1.0; CFAbsoluteTime secondFrameRenderTimestamp = - firstFrameRenderTimestamp + kFPRFrozenFrameThreshold + 0.005; // Buffer for float comparison. + firstFrameRenderTimestamp + frozenThreshold + 0.005; // Buffer for float comparison. id displayLinkMock = OCMClassMock([CADisplayLink class]); [self.tracker.displayLink invalidate]; self.tracker.displayLink = displayLinkMock; - // Set/Reset the previousFrameTimestamp if it has been set by a previous test. - OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp); + // Drive CADisplayLink with matched timestamp/targetTimestamp pairs per tick. + __block NSInteger tick = 0; + NSArray *timestamps = + @[ @(firstFrameRenderTimestamp), @(secondFrameRenderTimestamp) ]; + NSArray *targets = + @[ @(firstFrameRenderTimestamp + frameBudget), @(secondFrameRenderTimestamp + frameBudget) ]; + + OCMStub([displayLinkMock isPaused]).andReturn(NO); + + OCMStub([displayLinkMock timestamp]).andDo(^(NSInvocation *inv) { + CFTimeInterval v = timestamps[(NSUInteger)tick].doubleValue; + [inv setReturnValue:&v]; + }); + OCMStub([displayLinkMock targetTimestamp]).andDo(^(NSInvocation *inv) { + CFTimeInterval v = targets[(NSUInteger)tick].doubleValue; + [inv setReturnValue:&v]; + }); + + // Tick 1 - prime previous timestamp (no counts expected). [self.tracker displayLinkStep]; int64_t initialSlowFramesCount = self.tracker.slowFramesCount; int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount; - OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp); + // Tick 2 - frozen (also slow): duration > 42 * budget. + tick++; [self.tracker displayLinkStep]; int64_t newSlowFramesCount = self.tracker.slowFramesCount; int64_t newFrozenFramesCount = self.tracker.frozenFramesCount;