2828// Note: This was previously 60 FPS, but that resulted in 90% + of all frames collected to be
2929// flagged as slow frames, and so the threshold for iOS is being changed to 59 FPS.
3030// TODO(b/73498642): Make these configurable.
31+ // This constant is kept for backward compatibility but is no longer used directly.
32+ // The actual threshold is computed dynamically from UIScreen.maximumFramesPerSecond.
3133CFTimeInterval const kFPRSlowFrameThreshold = 1.0 / 59.0 ; // Anything less than 59 FPS is slow.
3234CFTimeInterval const kFPRFrozenFrameThreshold = 700.0 / 1000.0 ;
3335
36+ /* * Default/fallback FPS value used when UIScreen.maximumFramesPerSecond is unavailable or invalid.
37+ */
38+ static const NSInteger kFPRDefaultFPS = 60 ;
39+
40+ /* * Epsilon value to avoid floating point comparison issues (e.g., 59.94 vs 60). */
41+ static const CFTimeInterval kFPRSlowFrameEpsilon = 0.001 ;
42+
3443/* * Constant that indicates an invalid time. */
3544CFAbsoluteTime const kFPRInvalidTime = -1.0 ;
3645
@@ -80,6 +89,14 @@ @implementation FPRScreenTraceTracker {
8089
8190 /* * Instance variable storing the frozen frames observed so far. */
8291 atomic_int_fast64_t _frozenFramesCount;
92+
93+ /* * Cached maximum frames per second from UIScreen. */
94+ NSInteger _cachedMaxFPS;
95+
96+ /* * Cached slow frame budget computed from maxFPS. Initialized to the old constant value
97+ * for backward compatibility until updateCachedSlowBudget is called.
98+ */
99+ CFTimeInterval _cachedSlowBudget;
83100}
84101
85102@dynamic totalFramesCount;
@@ -112,6 +129,24 @@ - (instancetype)init {
112129 atomic_store_explicit (&_totalFramesCount, 0 , memory_order_relaxed);
113130 atomic_store_explicit (&_frozenFramesCount, 0 , memory_order_relaxed);
114131 atomic_store_explicit (&_slowFramesCount, 0 , memory_order_relaxed);
132+
133+ // Initialize cached values with defaults. These will be updated by updateCachedSlowBudget,
134+ // but having defaults ensures reasonable behavior if initialization is delayed or fails.
135+ _cachedMaxFPS = kFPRDefaultFPS ;
136+ _cachedSlowBudget = 1.0 / kFPRDefaultFPS ;
137+
138+ // Initialize cached maxFPS and slowBudget on main thread.
139+ // UIScreen.maximumFramesPerSecond reflects device capability and can be up to 120 on ProMotion.
140+ // TODO: Support ProMotion devices that dynamically adjust refresh rate based on content.
141+ // Use synchronous dispatch to ensure values are set before first frame is recorded.
142+ if ([NSThread isMainThread ]) {
143+ [self updateCachedSlowBudget ];
144+ } else {
145+ dispatch_sync (dispatch_get_main_queue (), ^{
146+ [self updateCachedSlowBudget ];
147+ });
148+ }
149+
115150 _displayLink = [CADisplayLink displayLinkWithTarget: self selector: @selector (displayLinkStep )];
116151 [_displayLink addToRunLoop: [NSRunLoop mainRunLoop ] forMode: NSRunLoopCommonModes ];
117152
@@ -126,6 +161,15 @@ - (instancetype)init {
126161 selector: @selector (appWillResignActiveNotification: )
127162 name: UIApplicationWillResignActiveNotification
128163 object: [UIApplication sharedApplication ]];
164+
165+ #if TARGET_OS_TV
166+ // On tvOS, the refresh rate can change when the user switches display modes or connects to
167+ // different displays. Listen for mode changes to recompute the slow budget.
168+ [[NSNotificationCenter defaultCenter ] addObserver: self
169+ selector: @selector (screenModeDidChangeNotification: )
170+ name: UIScreenModeDidChangeNotification
171+ object: nil ];
172+ #endif
129173 }
130174 return self;
131175}
@@ -139,6 +183,11 @@ - (void)dealloc {
139183 [[NSNotificationCenter defaultCenter ] removeObserver: self
140184 name: UIApplicationWillResignActiveNotification
141185 object: [UIApplication sharedApplication ]];
186+ #if TARGET_OS_TV
187+ [[NSNotificationCenter defaultCenter ] removeObserver: self
188+ name: UIScreenModeDidChangeNotification
189+ object: nil ];
190+ #endif
142191}
143192
144193- (void )appDidBecomeActiveNotification : (NSNotification *)notification {
@@ -183,13 +232,47 @@ - (void)appWillResignActiveNotification:(NSNotification *)notification {
183232 });
184233}
185234
235+ #if TARGET_OS_TV
236+ /* * Handles the UIScreenModeDidChangeNotification. Recomputes the cached slow budget when the screen
237+ * mode changes on tvOS.
238+ *
239+ * @param notification The NSNotification object.
240+ */
241+ - (void )screenModeDidChangeNotification : (NSNotification *)notification {
242+ dispatch_async (dispatch_get_main_queue (), ^{
243+ [self updateCachedSlowBudget ];
244+ });
245+ }
246+ #endif
247+
248+ /* * Updates the cached maxFPS and slowBudget from UIScreen.maximumFramesPerSecond.
249+ * This method must be called on the main thread.
250+ */
251+ - (void )updateCachedSlowBudget {
252+ NSAssert ([NSThread isMainThread ], @" updateCachedSlowBudget must be called on main thread" );
253+ UIScreen *mainScreen = [UIScreen mainScreen ];
254+ NSInteger maxFPS = 0 ;
255+ if (mainScreen) {
256+ maxFPS = mainScreen.maximumFramesPerSecond ;
257+ }
258+ if (maxFPS > 0 ) {
259+ _cachedMaxFPS = maxFPS;
260+ _cachedSlowBudget = 1.0 / maxFPS;
261+ } else {
262+ // Fallback to default FPS if maximumFramesPerSecond is unavailable or invalid.
263+ _cachedMaxFPS = kFPRDefaultFPS ;
264+ _cachedSlowBudget = 1.0 / kFPRDefaultFPS ;
265+ }
266+ }
267+
186268#pragma mark - Frozen, slow and good frames
187269
188270- (void )displayLinkStep {
189271 static CFAbsoluteTime previousTimestamp = kFPRInvalidTime ;
190272 CFAbsoluteTime currentTimestamp = self.displayLink .timestamp ;
273+ // Use the cached slow budget computed from UIScreen.maximumFramesPerSecond.
191274 RecordFrameType (currentTimestamp, previousTimestamp, &_slowFramesCount, &_frozenFramesCount,
192- &_totalFramesCount);
275+ &_totalFramesCount, _cachedSlowBudget );
193276 previousTimestamp = currentTimestamp;
194277}
195278
@@ -207,12 +290,15 @@ void RecordFrameType(CFAbsoluteTime currentTimestamp,
207290 CFAbsoluteTime previousTimestamp,
208291 atomic_int_fast64_t *slowFramesCounter,
209292 atomic_int_fast64_t *frozenFramesCounter,
210- atomic_int_fast64_t *totalFramesCounter) {
293+ atomic_int_fast64_t *totalFramesCounter,
294+ CFTimeInterval slowBudget) {
211295 CFTimeInterval frameDuration = currentTimestamp - previousTimestamp;
212296 if (previousTimestamp == kFPRInvalidTime ) {
213297 return ;
214298 }
215- if (frameDuration > kFPRSlowFrameThreshold ) {
299+ // Use cached slowBudget with epsilon to avoid floating point comparison issues
300+ // (e.g., 59.94 vs 60 Hz displays).
301+ if (frameDuration > slowBudget + kFPRSlowFrameEpsilon ) {
216302 atomic_fetch_add_explicit (slowFramesCounter, 1 , memory_order_relaxed);
217303 }
218304 if (frameDuration > kFPRFrozenFrameThreshold ) {
0 commit comments