Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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,8 +37,10 @@ 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 now uses UIScreen.maximumFramesPerSecond dynamically.
* New code should not rely on this value.
*/
FOUNDATION_EXTERN CFTimeInterval const kFPRSlowFrameThreshold;

Expand Down
29 changes: 25 additions & 4 deletions FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,35 @@
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.).
// For devices reporting 60 FPS, the threshold is approximately 16.67ms (1/60).
// 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 FPRSlowBudgetSeconds() which queries UIScreen.maximumFramesPerSecond dynamically.
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 slow frame budget in seconds based on the device's maximum FPS.
* A frame is considered slow if it takes longer than this duration to render.
*/
static inline CFTimeInterval FPRSlowBudgetSeconds(void) {
return 1.0 / (double)FPRMaxFPS();
}

/** Returns the class name without the prefixed module name present in Swift classes
* (e.g. MyModule.MyViewController -> MyViewController).
*/
Expand Down Expand Up @@ -212,7 +232,8 @@ void RecordFrameType(CFAbsoluteTime currentTimestamp,
if (previousTimestamp == kFPRInvalidTime) {
return;
}
if (frameDuration > kFPRSlowFrameThreshold) {
CFTimeInterval slowBudget = FPRSlowBudgetSeconds();
if (frameDuration > slowBudget) {
atomic_fetch_add_explicit(slowFramesCounter, 1, memory_order_relaxed);
}
if (frameDuration > kFPRFrozenFrameThreshold) {
Expand Down
189 changes: 189 additions & 0 deletions FirebasePerformance/Tests/Unit/FPRScreenTraceTrackerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -901,4 +901,193 @@ + (NSString *)expectedTraceNameForViewController:(UIViewController *)viewControl
return [@"_st_" stringByAppendingString:NSStringFromClass([viewController class])];
}

#pragma mark - Dynamic FPS Threshold Tests

/** Category to swizzle UIScreen.maximumFramesPerSecond for testing. */
@interface UIScreen (FPRTestSwizzle)
@property(nonatomic) NSInteger fpr_testMaxFPS;
@end

static NSInteger gFPRTestMaxFPS = 0;

@implementation UIScreen (FPRTestSwizzle)

- (NSInteger)fpr_swizzled_maximumFramesPerSecond {
if (gFPRTestMaxFPS > 0) {
return gFPRTestMaxFPS;
}
return [self fpr_swizzled_maximumFramesPerSecond]; // Call original implementation
}

- (void)setFpr_testMaxFPS:(NSInteger)fps {
gFPRTestMaxFPS = fps;
}

- (NSInteger)fpr_testMaxFPS {
return gFPRTestMaxFPS;
}

@end

/** Helper method to swizzle UIScreen.maximumFramesPerSecond for testing. */
static void FPRSwizzleMaxFPS(BOOL enable) {
static dispatch_once_t onceToken;
static Method originalMethod;
static Method swizzledMethod;

dispatch_once(&onceToken, ^{
Class class = [UIScreen class];
SEL originalSelector = @selector(maximumFramesPerSecond);
SEL swizzledSelector = @selector(fpr_swizzled_maximumFramesPerSecond);

originalMethod = class_getInstanceMethod(class, originalSelector);
swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
});

if (enable) {
method_exchangeImplementations(originalMethod, swizzledMethod);
} else {
method_exchangeImplementations(swizzledMethod, originalMethod);
gFPRTestMaxFPS = 0;
}
}

/** Tests that the slow frame threshold correctly adapts to 60 FPS displays.
* At 60 FPS, slow budget is ~16.67ms (1/60).
*/
- (void)testSlowThreshold60FPS {
FPRSwizzleMaxFPS(YES);
[[UIScreen mainScreen] setFpr_testMaxFPS:60];

CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
// 0.017s (17ms) should be slow for 60 FPS (budget ~16.67ms)
CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + 0.017;
// 0.016s (16ms) should NOT be slow for 60 FPS
CFAbsoluteTime thirdFrameRenderTimestamp = secondFrameRenderTimestamp + 0.016;
// 0.701s should be frozen
CFAbsoluteTime fourthFrameRenderTimestamp = thirdFrameRenderTimestamp + 0.701;

id displayLinkMock = OCMClassMock([CADisplayLink class]);
[self.tracker.displayLink invalidate];
self.tracker.displayLink = displayLinkMock;

// Reset previousFrameTimestamp
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
[self.tracker displayLinkStep];
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount;

// Test 17ms frame (should be slow)
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
[self.tracker displayLinkStep];
XCTAssertEqual(self.tracker.slowFramesCount, initialSlowFramesCount + 1);
XCTAssertEqual(self.tracker.frozenFramesCount, initialFrozenFramesCount);

// Test 16ms frame (should NOT be slow)
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
[self.tracker displayLinkStep];
XCTAssertEqual(self.tracker.slowFramesCount, initialSlowFramesCount + 1);
XCTAssertEqual(self.tracker.frozenFramesCount, initialFrozenFramesCount);

// Test 701ms frame (should be frozen and slow)
OCMExpect([displayLinkMock timestamp]).andReturn(fourthFrameRenderTimestamp);
[self.tracker displayLinkStep];
XCTAssertEqual(self.tracker.slowFramesCount, initialSlowFramesCount + 2);
XCTAssertEqual(self.tracker.frozenFramesCount, initialFrozenFramesCount + 1);

FPRSwizzleMaxFPS(NO);
}

/** Tests that the slow frame threshold correctly adapts to 120 FPS displays (ProMotion).
* At 120 FPS, slow budget is ~8.33ms (1/120).
*/
- (void)testSlowThreshold120FPS {
FPRSwizzleMaxFPS(YES);
[[UIScreen mainScreen] setFpr_testMaxFPS:120];

CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
// 0.009s (9ms) should be slow for 120 FPS (budget ~8.33ms)
CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + 0.009;
// 0.008s (8ms) should NOT be slow for 120 FPS
CFAbsoluteTime thirdFrameRenderTimestamp = secondFrameRenderTimestamp + 0.008;
// 0.701s should be frozen (unchanged threshold)
CFAbsoluteTime fourthFrameRenderTimestamp = thirdFrameRenderTimestamp + 0.701;

id displayLinkMock = OCMClassMock([CADisplayLink class]);
[self.tracker.displayLink invalidate];
self.tracker.displayLink = displayLinkMock;

// Reset previousFrameTimestamp
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
[self.tracker displayLinkStep];
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount;

// Test 9ms frame (should be slow)
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
[self.tracker displayLinkStep];
XCTAssertEqual(self.tracker.slowFramesCount, initialSlowFramesCount + 1);
XCTAssertEqual(self.tracker.frozenFramesCount, initialFrozenFramesCount);

// Test 8ms frame (should NOT be slow)
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
[self.tracker displayLinkStep];
XCTAssertEqual(self.tracker.slowFramesCount, initialSlowFramesCount + 1);
XCTAssertEqual(self.tracker.frozenFramesCount, initialFrozenFramesCount);

// Test 701ms frame (should be frozen and slow)
OCMExpect([displayLinkMock timestamp]).andReturn(fourthFrameRenderTimestamp);
[self.tracker displayLinkStep];
XCTAssertEqual(self.tracker.slowFramesCount, initialSlowFramesCount + 2);
XCTAssertEqual(self.tracker.frozenFramesCount, initialFrozenFramesCount + 1);

FPRSwizzleMaxFPS(NO);
}

/** Tests that the slow frame threshold correctly adapts to 50 FPS displays (some tvOS devices).
* At 50 FPS, slow budget is 20ms (1/50).
*/
- (void)testSlowThreshold50FPS {
FPRSwizzleMaxFPS(YES);
[[UIScreen mainScreen] setFpr_testMaxFPS:50];

CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
// 0.021s (21ms) should be slow for 50 FPS (budget 20ms)
CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + 0.021;
// 0.019s (19ms) should NOT be slow for 50 FPS
CFAbsoluteTime thirdFrameRenderTimestamp = secondFrameRenderTimestamp + 0.019;
// 0.701s should be frozen (unchanged threshold)
CFAbsoluteTime fourthFrameRenderTimestamp = thirdFrameRenderTimestamp + 0.701;

id displayLinkMock = OCMClassMock([CADisplayLink class]);
[self.tracker.displayLink invalidate];
self.tracker.displayLink = displayLinkMock;

// Reset previousFrameTimestamp
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
[self.tracker displayLinkStep];
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount;

// Test 21ms frame (should be slow)
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
[self.tracker displayLinkStep];
XCTAssertEqual(self.tracker.slowFramesCount, initialSlowFramesCount + 1);
XCTAssertEqual(self.tracker.frozenFramesCount, initialFrozenFramesCount);

// Test 19ms frame (should NOT be slow)
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
[self.tracker displayLinkStep];
XCTAssertEqual(self.tracker.slowFramesCount, initialSlowFramesCount + 1);
XCTAssertEqual(self.tracker.frozenFramesCount, initialFrozenFramesCount);

// Test 701ms frame (should be frozen and slow)
OCMExpect([displayLinkMock timestamp]).andReturn(fourthFrameRenderTimestamp);
[self.tracker displayLinkStep];
XCTAssertEqual(self.tracker.slowFramesCount, initialSlowFramesCount + 2);
XCTAssertEqual(self.tracker.frozenFramesCount, initialFrozenFramesCount + 1);

FPRSwizzleMaxFPS(NO);
}

@end