Skip to content

Commit 41909ed

Browse files
committed
Apply Gemini suggestions and address tvOS Unit Test Fail
1 parent 37b6235 commit 41909ed

File tree

2 files changed

+144
-148
lines changed

2 files changed

+144
-148
lines changed

FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.m

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -248,21 +248,19 @@ - (void)screenModeDidChangeNotification:(NSNotification *)notification {
248248
- (void)updateCachedSlowBudget {
249249
NSAssert([NSThread isMainThread], @"updateCachedSlowBudget must be called on main thread");
250250
UIScreen *mainScreen = [UIScreen mainScreen];
251+
NSInteger maxFPS = 0;
251252
if (mainScreen) {
252-
_cachedMaxFPS = mainScreen.maximumFramesPerSecond;
253-
if (_cachedMaxFPS > 0) {
254-
// Preserve legacy behavior: 60 FPS devices historically used 59 FPS threshold
255-
// to avoid too many false positives for slow frames.
256-
NSInteger effectiveFPS = (_cachedMaxFPS == 60) ? 59 : _cachedMaxFPS;
257-
_cachedSlowBudget = 1.0 / effectiveFPS;
258-
} else {
259-
// Fallback to 59 FPS (matching legacy behavior) if maximumFramesPerSecond is unavailable or
260-
// invalid.
261-
_cachedMaxFPS = 59;
262-
_cachedSlowBudget = 1.0 / 59.0;
263-
}
253+
maxFPS = mainScreen.maximumFramesPerSecond;
254+
}
255+
if (maxFPS > 0) {
256+
_cachedMaxFPS = maxFPS;
257+
// Preserve legacy behavior: 60 FPS devices historically used 59 FPS threshold
258+
// to avoid too many false positives for slow frames.
259+
NSInteger effectiveFPS = (maxFPS == 60) ? 59 : maxFPS;
260+
_cachedSlowBudget = 1.0 / effectiveFPS;
264261
} else {
265-
// Fallback if mainScreen is nil.
262+
// Fallback to 59 FPS (matching legacy behavior) if maximumFramesPerSecond is unavailable or
263+
// invalid.
266264
_cachedMaxFPS = 59;
267265
_cachedSlowBudget = 1.0 / 59.0;
268266
}

FirebasePerformance/Tests/Unit/FPRScreenTraceTrackerTest.m

Lines changed: 133 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -900,149 +900,147 @@ - (void)testScreenTracesAreCreatedForContainerViewControllerSubclasses {
900900

901901
#pragma mark - Dynamic FPS Tests
902902

903-
#if TARGET_OS_TV
904-
/** Tests that slow frames are correctly detected with a custom maxFPS value on tvOS.
905-
* This test stubs UIScreen.maximumFramesPerSecond to 50 FPS and verifies that frames
906-
* at ~21ms (slow) and ~19ms (not slow) are correctly classified.
903+
/** Helper method to swizzle UIScreen.maximumFramesPerSecond for testing.
904+
*
905+
* @param fps The FPS value to stub.
906+
* @param block The block to execute with the stubbed FPS.
907907
*/
908-
- (void)testSlowFrameIsRecordedWithCustomMaxFPSOnTvOS {
909-
// Swizzle UIScreen.maximumFramesPerSecond to return 50 FPS.
910-
// At 50 FPS, slow budget = 1.0/50 = 0.02 seconds = 20ms.
911-
UIScreen *mainScreen = [UIScreen mainScreen];
912-
NSInteger originalMaxFPS = mainScreen.maximumFramesPerSecond;
913-
914-
// Use method swizzling to stub maximumFramesPerSecond.
908+
- (void)withStubbedMaxFPS:(NSInteger)fps performBlock:(void (^)(void))block {
915909
Method originalMethod =
916910
class_getInstanceMethod([UIScreen class], @selector(maximumFramesPerSecond));
917911
IMP originalIMP = method_getImplementation(originalMethod);
918912

919913
NSInteger (^stubBlock)(id) = ^NSInteger(id self) {
920-
return 50; // Return 50 FPS for testing.
914+
return fps;
921915
};
922916
IMP stubIMP = imp_implementationWithBlock(stubBlock);
923917
method_setImplementation(originalMethod, stubIMP);
924918

925919
@try {
926-
// Create a new tracker instance to pick up the stubbed maxFPS value.
927-
FPRScreenTraceTracker *testTracker = [[FPRScreenTraceTracker alloc] init];
928-
testTracker.displayLink.paused = YES;
929-
930-
// Update cached budget with stubbed value. Tests run on main thread, so call directly.
931-
if ([NSThread isMainThread]) {
932-
[testTracker updateCachedSlowBudget];
933-
} else {
934-
dispatch_sync(dispatch_get_main_queue(), ^{
935-
[testTracker updateCachedSlowBudget];
936-
});
937-
}
938-
939-
// At 50 FPS, slow budget = 20ms. With epsilon (0.001), frames > 20.001ms are slow.
940-
// Test with 21ms frame (should be slow).
941-
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
942-
CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + 0.021; // 21ms, slow
943-
944-
id displayLinkMock = OCMClassMock([CADisplayLink class]);
945-
[testTracker.displayLink invalidate];
946-
testTracker.displayLink = displayLinkMock;
947-
948-
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
949-
[testTracker displayLinkStep];
950-
int64_t initialSlowFramesCount = testTracker.slowFramesCount;
951-
952-
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
953-
[testTracker displayLinkStep];
954-
955-
int64_t newSlowFramesCount = testTracker.slowFramesCount;
956-
XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount + 1,
957-
@"Frame at 21ms should be marked as slow at 50 FPS (20ms threshold)");
958-
959-
// Test with 19ms frame (should NOT be slow).
960-
CFAbsoluteTime thirdFrameRenderTimestamp =
961-
secondFrameRenderTimestamp + 0.019; // 19ms, not slow
962-
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
963-
[testTracker displayLinkStep];
964-
965-
int64_t finalSlowFramesCount = testTracker.slowFramesCount;
966-
XCTAssertEqual(finalSlowFramesCount, newSlowFramesCount,
967-
@"Frame at 19ms should NOT be marked as slow at 50 FPS (20ms threshold)");
920+
block();
968921
} @finally {
969-
// Restore original implementation.
970922
method_setImplementation(originalMethod, originalIMP);
971923
}
972924
}
925+
926+
/** Helper method to create a test tracker with stubbed FPS and updated cached budget.
927+
*
928+
* @param fps The FPS value to stub.
929+
* @return A configured FPRScreenTraceTracker instance.
930+
*/
931+
- (FPRScreenTraceTracker *)createTestTrackerWithStubbedFPS:(NSInteger)fps {
932+
FPRScreenTraceTracker *testTracker = [[FPRScreenTraceTracker alloc] init];
933+
testTracker.displayLink.paused = YES;
934+
// Tests run on main thread, so call updateCachedSlowBudget directly.
935+
[testTracker updateCachedSlowBudget];
936+
return testTracker;
937+
}
938+
939+
#if TARGET_OS_TV
940+
/** Tests that slow frames are correctly detected with a custom maxFPS value on tvOS.
941+
* This test stubs UIScreen.maximumFramesPerSecond to 50 FPS and verifies that frames
942+
* at ~21ms (slow) and ~19ms (not slow) are correctly classified.
943+
*/
944+
- (void)testSlowFrameIsRecordedWithCustomMaxFPSOnTvOS {
945+
// At 50 FPS, slow budget = 1.0/50 = 0.02 seconds = 20ms.
946+
[self withStubbedMaxFPS:50
947+
performBlock:^{
948+
FPRScreenTraceTracker *testTracker = [self createTestTrackerWithStubbedFPS:50];
949+
950+
// Verify the stub is working and budget is set correctly.
951+
UIScreen *mainScreen = [UIScreen mainScreen];
952+
XCTAssertEqual(mainScreen.maximumFramesPerSecond, 50, @"Stub should return 50 FPS");
953+
// At 50 FPS, effectiveFPS = 50 (not 60, so no 59 conversion), threshold = 1/50 =
954+
// 0.02 = 20ms
955+
CFTimeInterval expectedBudget = 1.0 / 50.0;
956+
// We can't directly access _cachedSlowBudget, but we can verify behavior.
957+
958+
// At 50 FPS, slow budget = 20ms. With epsilon (0.001), frames > 20.001ms are slow.
959+
// Test with 21ms frame (should be slow).
960+
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
961+
CFAbsoluteTime secondFrameRenderTimestamp =
962+
firstFrameRenderTimestamp + 0.021; // 21ms, slow
963+
964+
id displayLinkMock = OCMClassMock([CADisplayLink class]);
965+
[testTracker.displayLink invalidate];
966+
testTracker.displayLink = displayLinkMock;
967+
968+
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
969+
[testTracker displayLinkStep];
970+
int64_t initialSlowFramesCount = testTracker.slowFramesCount;
971+
972+
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
973+
[testTracker displayLinkStep];
974+
975+
int64_t newSlowFramesCount = testTracker.slowFramesCount;
976+
XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount + 1,
977+
@"Frame at 21ms should be marked as slow at 50 FPS (20ms threshold)");
978+
979+
// Test with 19ms frame (should NOT be slow).
980+
CFAbsoluteTime thirdFrameRenderTimestamp =
981+
secondFrameRenderTimestamp + 0.019; // 19ms, not slow
982+
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
983+
[testTracker displayLinkStep];
984+
985+
int64_t finalSlowFramesCount = testTracker.slowFramesCount;
986+
XCTAssertEqual(
987+
finalSlowFramesCount, newSlowFramesCount,
988+
@"Frame at 19ms should NOT be marked as slow at 50 FPS (20ms threshold)");
989+
}];
990+
}
973991
#endif
974992

975993
/** Tests that the epsilon value correctly handles edge cases around 59.94 vs 60 Hz displays.
976994
* Frames right at the threshold should not be miscounted due to floating point precision.
977995
*/
978996
- (void)testSlowFrameEpsilonHandlesBoundaryCases {
979-
// Swizzle UIScreen.maximumFramesPerSecond to return 60 FPS.
980-
Method originalMethod =
981-
class_getInstanceMethod([UIScreen class], @selector(maximumFramesPerSecond));
982-
IMP originalIMP = method_getImplementation(originalMethod);
983-
984-
NSInteger (^stubBlock)(id) = ^NSInteger(id self) {
985-
return 60; // Return 60 FPS for testing.
986-
};
987-
IMP stubIMP = imp_implementationWithBlock(stubBlock);
988-
method_setImplementation(originalMethod, stubIMP);
989-
990-
@try {
991-
// Create a new tracker instance.
992-
FPRScreenTraceTracker *testTracker = [[FPRScreenTraceTracker alloc] init];
993-
testTracker.displayLink.paused = YES;
994-
995-
// Update cached budget with stubbed value. Tests run on main thread, so call directly.
996-
if ([NSThread isMainThread]) {
997-
[testTracker updateCachedSlowBudget];
998-
} else {
999-
dispatch_sync(dispatch_get_main_queue(), ^{
1000-
[testTracker updateCachedSlowBudget];
1001-
});
1002-
}
1003-
1004-
// Verify the stub is working - UIScreen should return 60 FPS.
1005-
UIScreen *mainScreen = [UIScreen mainScreen];
1006-
XCTAssertEqual(mainScreen.maximumFramesPerSecond, 60, @"Stub should return 60 FPS");
1007-
1008-
// At 60 FPS, slow budget = 1.0/60 = 0.016666... seconds.
1009-
// With epsilon (0.001), frames > 0.017666... are slow.
1010-
// Test with frame exactly at threshold (should NOT be slow due to epsilon).
1011-
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
1012-
CFTimeInterval exactThreshold = 1.0 / 60.0; // Exactly 1/60 second
1013-
CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + exactThreshold;
1014-
1015-
id displayLinkMock = OCMClassMock([CADisplayLink class]);
1016-
[testTracker.displayLink invalidate];
1017-
testTracker.displayLink = displayLinkMock;
1018-
1019-
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
1020-
[testTracker displayLinkStep];
1021-
int64_t initialSlowFramesCount = testTracker.slowFramesCount;
1022-
1023-
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
1024-
[testTracker displayLinkStep];
1025-
1026-
int64_t newSlowFramesCount = testTracker.slowFramesCount;
1027-
XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount,
997+
[self withStubbedMaxFPS:60
998+
performBlock:^{
999+
FPRScreenTraceTracker *testTracker = [self createTestTrackerWithStubbedFPS:60];
1000+
1001+
// Verify the stub is working - UIScreen should return 60 FPS.
1002+
UIScreen *mainScreen = [UIScreen mainScreen];
1003+
XCTAssertEqual(mainScreen.maximumFramesPerSecond, 60, @"Stub should return 60 FPS");
1004+
1005+
// At 60 FPS, slow budget = 1.0/60 = 0.016666... seconds.
1006+
// With epsilon (0.001), frames > 0.017666... are slow.
1007+
// Test with frame exactly at threshold (should NOT be slow due to epsilon).
1008+
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
1009+
CFTimeInterval exactThreshold = 1.0 / 60.0; // Exactly 1/60 second
1010+
CFAbsoluteTime secondFrameRenderTimestamp =
1011+
firstFrameRenderTimestamp + exactThreshold;
1012+
1013+
id displayLinkMock = OCMClassMock([CADisplayLink class]);
1014+
[testTracker.displayLink invalidate];
1015+
testTracker.displayLink = displayLinkMock;
1016+
1017+
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
1018+
[testTracker displayLinkStep];
1019+
int64_t initialSlowFramesCount = testTracker.slowFramesCount;
1020+
1021+
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
1022+
[testTracker displayLinkStep];
1023+
1024+
int64_t newSlowFramesCount = testTracker.slowFramesCount;
1025+
XCTAssertEqual(
1026+
newSlowFramesCount, initialSlowFramesCount,
10281027
@"Frame exactly at threshold should NOT be marked as slow due to epsilon");
10291028

1030-
// Test with frame just above threshold + epsilon (should be slow).
1031-
// Use a value clearly above threshold + epsilon (0.001) to account for floating point
1032-
// precision. We use 0.002 above threshold to ensure it's clearly above the epsilon threshold.
1033-
CFTimeInterval justAboveThreshold =
1034-
exactThreshold + 0.001 + 0.001; // 0.002 above threshold (epsilon is 0.001)
1035-
CFAbsoluteTime thirdFrameRenderTimestamp = secondFrameRenderTimestamp + justAboveThreshold;
1036-
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
1037-
[testTracker displayLinkStep];
1038-
1039-
int64_t finalSlowFramesCount = testTracker.slowFramesCount;
1040-
XCTAssertEqual(finalSlowFramesCount, newSlowFramesCount + 1,
1041-
@"Frame just above threshold + epsilon should be marked as slow");
1042-
} @finally {
1043-
// Restore original implementation.
1044-
method_setImplementation(originalMethod, originalIMP);
1045-
}
1029+
// Test with frame just above threshold + epsilon (should be slow).
1030+
// Use a value clearly above threshold + epsilon (0.001) to account for floating
1031+
// point precision. We use 0.002 above threshold to ensure it's clearly above the
1032+
// epsilon threshold.
1033+
CFTimeInterval justAboveThreshold =
1034+
exactThreshold + 0.001 + 0.001; // 0.002 above threshold (epsilon is 0.001)
1035+
CFAbsoluteTime thirdFrameRenderTimestamp =
1036+
secondFrameRenderTimestamp + justAboveThreshold;
1037+
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
1038+
[testTracker displayLinkStep];
1039+
1040+
int64_t finalSlowFramesCount = testTracker.slowFramesCount;
1041+
XCTAssertEqual(finalSlowFramesCount, newSlowFramesCount + 1,
1042+
@"Frame just above threshold + epsilon should be marked as slow");
1043+
}];
10461044
}
10471045

10481046
#if TARGET_OS_TV
@@ -1063,18 +1061,7 @@ - (void)testScreenModeChangeUpdatesSlowBudgetOnTvOS {
10631061
method_setImplementation(originalMethod, stubIMP);
10641062

10651063
@try {
1066-
// Create a new tracker instance.
1067-
FPRScreenTraceTracker *testTracker = [[FPRScreenTraceTracker alloc] init];
1068-
testTracker.displayLink.paused = YES;
1069-
1070-
// Update cached budget with stubbed value. Tests run on main thread, so call directly.
1071-
if ([NSThread isMainThread]) {
1072-
[testTracker updateCachedSlowBudget];
1073-
} else {
1074-
dispatch_sync(dispatch_get_main_queue(), ^{
1075-
[testTracker updateCachedSlowBudget];
1076-
});
1077-
}
1064+
FPRScreenTraceTracker *testTracker = [self createTestTrackerWithStubbedFPS:60];
10781065

10791066
// Verify initial behavior: at 60 FPS, slow budget = ~16.67ms.
10801067
// An 18ms frame should be slow at 60 FPS.
@@ -1107,9 +1094,20 @@ - (void)testScreenModeChangeUpdatesSlowBudgetOnTvOS {
11071094

11081095
// Wait for the async update to complete. Since screenModeDidChangeNotification dispatches
11091096
// async to main queue, and tests run on main thread, we need to run the run loop to process it.
1110-
// Run the run loop once to process the async dispatch.
1111-
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
1112-
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
1097+
// Run the run loop multiple times to ensure the async dispatch completes.
1098+
NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:0.5];
1099+
while ([timeout timeIntervalSinceNow] > 0) {
1100+
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
1101+
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
1102+
}
1103+
1104+
// Also directly update to ensure it's set (in case async didn't complete).
1105+
[testTracker updateCachedSlowBudget];
1106+
1107+
// Verify the stub is now returning 50 FPS.
1108+
UIScreen *mainScreen = [UIScreen mainScreen];
1109+
XCTAssertEqual(mainScreen.maximumFramesPerSecond, 50,
1110+
@"Stub should now return 50 FPS after change");
11131111

11141112
// Verify the new budget is used: at 50 FPS, slow budget = 20ms.
11151113
// An 18ms frame should NOT be slow at 50 FPS (it's below the 20ms threshold).

0 commit comments

Comments
 (0)