Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d352bc8
Merge remote-tracking branch 'origin/main' into JesusRojass/#10220Fix
JesusRojass Nov 11, 2025
cf41b42
Use maximumFramesPerSecond for slow-frame threshold
JesusRojass Nov 11, 2025
56dad5f
Fix global static variable - Gemini Suggestion, moved some code around
JesusRojass Nov 11, 2025
2401746
Simplify tests and run style.sh
JesusRojass Nov 11, 2025
189948d
Address Gemini comments - dynamic slowBudget
JesusRojass Nov 11, 2025
c9a3d05
Removed redundant tests
JesusRojass Nov 11, 2025
6eabdb1
Update FirebasePerformance/Tests/Unit/FPRScreenTraceTrackerTest.m
JesusRojass Nov 11, 2025
c608a11
Code Cleanup - Gemini Suggestion
JesusRojass Nov 11, 2025
7b14e4c
Optimize method swizzling and remove unused properties
JesusRojass Nov 11, 2025
c464b2e
Remove Redundant block of code
JesusRojass Nov 11, 2025
4407c40
Separate tests and clean stuff up
JesusRojass Nov 11, 2025
fc10cc9
Fix curly braces
JesusRojass Nov 11, 2025
04515e6
Cache slow frame budget and implement a refresh on app active
JesusRojass Nov 11, 2025
d1056fc
Update FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m
JesusRojass Nov 11, 2025
3c876a5
Update FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m
JesusRojass Nov 11, 2025
a0d52d2
Update FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m
JesusRojass Nov 11, 2025
0f204df
Consolidate all app did become active logic into one place
JesusRojass Nov 11, 2025
e172aad
Added descriptive messages to the XCTAssertEqual
JesusRojass Nov 11, 2025
2a0d6cd
add a tearDown method to ensure test isolation
JesusRojass Nov 11, 2025
222bbf9
Refactor previousTimestamp into an instance variable
JesusRojass Nov 11, 2025
47d04c9
refactor previousTimestamp into an instance variable
JesusRojass Nov 11, 2025
a2131eb
tidy up!
JesusRojass Nov 11, 2025
98048a3
added fpr_refreshFrameRateCache method - gemini suggestion
JesusRojass Nov 11, 2025
0a7b38e
Make the fpr_refreshFrameRateCache logic more explicit - gemini suggests
JesusRojass Nov 11, 2025
67804ce
Update FirebasePerformance/Tests/Unit/FPRScreenTraceTrackerTest.m
JesusRojass Nov 11, 2025
368c528
Address Comments - tvOS-only refresh and round up amd add tvOS tests
JesusRojass Nov 13, 2025
e145748
Needed test adaptations
JesusRojass Nov 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ 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.
/** Legacy slow frame threshold constant (formerly 1/59).
* NOTE: This constant is deprecated and maintained only for test compatibility.
* The actual slow frame detection uses a cached value computed from
* UIScreen.maximumFramesPerSecond (slow threshold = 1000 / maxFPS ms).
* New code should not rely on this value.
*/
FOUNDATION_EXTERN CFTimeInterval const kFPRSlowFrameThreshold;

/** Frozen frame threshold (for time difference between current and previous frame render time)
* in sec.
* in sec. Frozen threshold = 700 ms (>700 ms).
*/
FOUNDATION_EXTERN CFTimeInterval const kFPRFrozenFrameThreshold;

Expand Down Expand Up @@ -81,6 +84,12 @@ FOUNDATION_EXTERN CFTimeInterval const kFPRFrozenFrameThreshold;
/** The slow frames counter. */
@property(atomic) int_fast64_t slowFramesCount;

/** The previous frame timestamp from the display link. Used to calculate frame duration. */
@property(nonatomic) CFAbsoluteTime previousTimestamp;

/** Refreshes the cached maximum FPS and slow frame budget from UIScreen. */
- (void)fpr_refreshFrameRateCache;

/** Handles the appDidBecomeActive notification. Restores the screen traces that were active before
* the app was backgrounded.
*
Expand Down
72 changes: 64 additions & 8 deletions FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,30 @@
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.
// Note: The slow frame threshold is now dynamically computed based on UIScreen's
// maximumFramesPerSecond to align with device capabilities (ProMotion, tvOS, etc.).
// Slow threshold = 1000 / UIScreen.maximumFramesPerSecond ms (or 1.0 / maxFPS seconds).
// For devices reporting 60 FPS, the threshold is approximately 16.67ms (1/60).
// The threshold is cached and refreshed when the app becomes active.
// TODO(b/73498642): Make these configurable.
CFTimeInterval const kFPRSlowFrameThreshold = 1.0 / 59.0; // Anything less than 59 FPS is slow.

// Legacy constant maintained for test compatibility. The actual slow frame detection
// uses a cached value computed from UIScreen.maximumFramesPerSecond.
CFTimeInterval const kFPRSlowFrameThreshold = 1.0 / 60.0;

CFTimeInterval const kFPRFrozenFrameThreshold = 700.0 / 1000.0;

/** Constant that indicates an invalid time. */
CFAbsoluteTime const kFPRInvalidTime = -1.0;

/** Returns the maximum frames per second supported by the device's main screen.
* Falls back to 60 if the value is unavailable or invalid.
*/
static inline NSInteger FPRMaxFPS(void) {
NSInteger maxFPS = [UIScreen mainScreen].maximumFramesPerSecond;
return maxFPS > 0 ? maxFPS : 60;
}

/** Returns the class name without the prefixed module name present in Swift classes
* (e.g. MyModule.MyViewController -> MyViewController).
*/
Expand Down Expand Up @@ -71,6 +86,19 @@
}
}

@interface FPRScreenTraceTracker ()

// fpr_cachedMaxFPS and fpr_cachedSlowBudget are initialized at startup.
// We update them only on tvOS during appDidBecomeActive because the output
// mode can change with user settings. iOS ProMotion dynamic changes are
// intentionally left for a future follow-up (see TODO in the notification
// handler).
@property(nonatomic) NSInteger fpr_cachedMaxFPS;

@property(nonatomic) CFTimeInterval fpr_cachedSlowBudget;

@end

@implementation FPRScreenTraceTracker {
/** Instance variable storing the total frames observed so far. */
atomic_int_fast64_t _totalFramesCount;
Expand Down Expand Up @@ -112,6 +140,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);
_previousTimestamp = kFPRInvalidTime;
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkStep)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

Expand All @@ -126,6 +155,17 @@ - (instancetype)init {
selector:@selector(appWillResignActiveNotification:)
name:UIApplicationWillResignActiveNotification
object:[UIApplication sharedApplication]];

// Initialize cached FPS and slow budget
NSInteger __fps = UIScreen.mainScreen.maximumFramesPerSecond ?: 60;
#if TARGET_OS_TV
// tvOS may report 59 for ~60Hz outputs. Normalize to 60.
if (__fps == 59) {
__fps = 60;
}
#endif
self.fpr_cachedMaxFPS = __fps;
self.fpr_cachedSlowBudget = 1.0 / (double)__fps;
}
return self;
}
Expand All @@ -142,6 +182,20 @@ - (void)dealloc {
}

- (void)appDidBecomeActiveNotification:(NSNotification *)notification {
#if TARGET_OS_TV
NSInteger __fps = UIScreen.mainScreen.maximumFramesPerSecond ?: 60;
if (__fps == 59) {
__fps = 60; // normalize tvOS 59 -> 60
}
if (__fps != self.fpr_cachedMaxFPS) {
self.fpr_cachedMaxFPS = __fps;
self.fpr_cachedSlowBudget = 1.0 / (double)__fps;
}
#else
// TODO: Support dynamic ProMotion changes on iOS in a future follow-up.
// For now, do not refresh here to avoid incorrect assumptions about timing.
#endif

// 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);
Expand Down Expand Up @@ -186,33 +240,35 @@ - (void)appWillResignActiveNotification:(NSNotification *)notification {
#pragma mark - Frozen, slow and good frames

- (void)displayLinkStep {
static CFAbsoluteTime previousTimestamp = kFPRInvalidTime;
CFAbsoluteTime currentTimestamp = self.displayLink.timestamp;
RecordFrameType(currentTimestamp, previousTimestamp, &_slowFramesCount, &_frozenFramesCount,
&_totalFramesCount);
previousTimestamp = currentTimestamp;
CFTimeInterval slowBudget = self.fpr_cachedSlowBudget;
RecordFrameType(currentTimestamp, self.previousTimestamp, slowBudget, &_slowFramesCount,
&_frozenFramesCount, &_totalFramesCount);
self.previousTimestamp = currentTimestamp;
}

/** This function increments the relevant frame counters based on the current and previous
* timestamp provided by the displayLink.
*
* @param currentTimestamp The current timestamp of the displayLink.
* @param previousTimestamp The previous timestamp of the displayLink.
* @param slowBudget The cached slow frame budget in seconds (1.0 / maxFPS).
* @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.
*/
FOUNDATION_STATIC_INLINE
void RecordFrameType(CFAbsoluteTime currentTimestamp,
CFAbsoluteTime previousTimestamp,
CFTimeInterval slowBudget,
atomic_int_fast64_t *slowFramesCounter,
atomic_int_fast64_t *frozenFramesCounter,
atomic_int_fast64_t *totalFramesCounter) {
CFTimeInterval frameDuration = currentTimestamp - previousTimestamp;
if (previousTimestamp == kFPRInvalidTime) {
return;
}
if (frameDuration > kFPRSlowFrameThreshold) {
if (frameDuration > slowBudget) {
atomic_fetch_add_explicit(slowFramesCounter, 1, memory_order_relaxed);
}
if (frameDuration > kFPRFrozenFrameThreshold) {
Expand Down
Loading