Skip to content

Commit 31f3a86

Browse files
authored
#10220 - Use UIScreen.maximumFramesPerSecond for dynamic slow frame threshold (#15516)
1 parent 26388b6 commit 31f3a86

File tree

3 files changed

+347
-4
lines changed

3 files changed

+347
-4
lines changed

FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker+Private.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,20 @@ FOUNDATION_EXTERN CFTimeInterval const kFPRFrozenFrameThreshold;
112112
*/
113113
- (void)viewControllerDidDisappear:(id)viewController;
114114

115+
#if TARGET_OS_TV
116+
/** Handles the UIScreenModeDidChangeNotification. Recomputes the cached slow budget when the screen
117+
* mode changes on tvOS.
118+
*
119+
* @param notification The NSNotification object.
120+
*/
121+
- (void)screenModeDidChangeNotification:(NSNotification *)notification;
122+
#endif
123+
124+
/** Updates the cached maxFPS and slowBudget from UIScreen.maximumFramesPerSecond.
125+
* This method must be called on the main thread.
126+
*/
127+
- (void)updateCachedSlowBudget;
128+
115129
@end
116130

117131
NS_ASSUME_NONNULL_END

FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@
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.
3133
CFTimeInterval const kFPRSlowFrameThreshold = 1.0 / 59.0; // Anything less than 59 FPS is slow.
3234
CFTimeInterval 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. */
3544
CFAbsoluteTime 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

Comments
 (0)