@@ -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