mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
1340 lines
59 KiB
Objective-C
1340 lines
59 KiB
Objective-C
// Copyright 2020 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#import "FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker+Private.h"
|
|
#import "FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.h"
|
|
|
|
#import <XCTest/XCTest.h>
|
|
#import <objc/runtime.h>
|
|
|
|
#import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h"
|
|
|
|
#import <OCMock/OCMock.h>
|
|
#import "FirebasePerformance/Tests/Unit/FPRTestCase.h"
|
|
|
|
/** Registers and returns an instance of a custom subclass of UIViewController. */
|
|
static UIViewController *FPRCustomViewController(NSString *className, BOOL isViewLoaded) {
|
|
Class customClass = NSClassFromString(className);
|
|
if (!customClass) {
|
|
// Register the class if it does not already exist.
|
|
customClass = objc_allocateClassPair([UIViewController class], className.UTF8String, 0);
|
|
objc_registerClassPair(customClass);
|
|
}
|
|
|
|
UIViewController *customVC = [[customClass alloc] init];
|
|
|
|
if (isViewLoaded) {
|
|
[customVC view];
|
|
}
|
|
return customVC;
|
|
}
|
|
|
|
/** Test UINavigationController subclass. */
|
|
@interface FPRTestNavigationViewController : UINavigationController
|
|
@end
|
|
|
|
@implementation FPRTestNavigationViewController
|
|
@end
|
|
|
|
/** Test UITabBarController subclass. */
|
|
@interface FPRTestTabBarController : UITabBarController
|
|
@end
|
|
|
|
@implementation FPRTestTabBarController
|
|
@end
|
|
|
|
/** Test UISplitViewController subclass. */
|
|
@interface FPRTestSplitViewController : UISplitViewController
|
|
@end
|
|
|
|
@implementation FPRTestSplitViewController
|
|
@end
|
|
|
|
/** Test UIPageViewController. */
|
|
@interface FPRTestPageViewController : UIPageViewController
|
|
@end
|
|
|
|
@implementation FPRTestPageViewController
|
|
@end
|
|
|
|
@interface FPRScreenTraceTrackerTest : FPRTestCase
|
|
|
|
/** The FPRScreenTraceTracker instance that's being used for a given test. */
|
|
@property(nonatomic, nullable) FPRScreenTraceTracker *tracker;
|
|
|
|
/** The dispatch group a test should wait for completion on before asserting behavior under test. */
|
|
@property(nonatomic, nullable) dispatch_group_t dispatchGroupToWaitOn;
|
|
|
|
@end
|
|
|
|
@implementation FPRScreenTraceTrackerTest
|
|
|
|
- (void)setUp {
|
|
[super setUp];
|
|
|
|
FIRPerformance *performance = [FIRPerformance sharedInstance];
|
|
[performance setDataCollectionEnabled:YES];
|
|
self.tracker = [[FPRScreenTraceTracker alloc] init];
|
|
self.tracker.displayLink.paused = YES;
|
|
self.dispatchGroupToWaitOn = self.tracker.screenTraceTrackerDispatchGroup;
|
|
}
|
|
|
|
- (void)tearDown {
|
|
[super tearDown];
|
|
|
|
FIRPerformance *performance = [FIRPerformance sharedInstance];
|
|
[performance setDataCollectionEnabled:NO];
|
|
self.tracker = nil;
|
|
self.dispatchGroupToWaitOn = nil;
|
|
}
|
|
|
|
/** Tests that shared instance returns the same instance. */
|
|
- (void)testSingleton {
|
|
FPRScreenTraceTracker *trackerOne = [FPRScreenTraceTracker sharedInstance];
|
|
FPRScreenTraceTracker *trackerTwo = [FPRScreenTraceTracker sharedInstance];
|
|
|
|
XCTAssertEqual(trackerOne, trackerTwo); // Check that it's the same instance.
|
|
}
|
|
|
|
/** Tests that the atomic counters are initialized to zero during init. */
|
|
- (void)testCountersInitToZero {
|
|
FPRScreenTraceTracker *tracker = [[FPRScreenTraceTracker alloc] init];
|
|
XCTAssertEqual(tracker.frozenFramesCount, 0);
|
|
XCTAssertEqual(tracker.slowFramesCount, 0);
|
|
XCTAssertEqual(tracker.totalFramesCount, 0);
|
|
}
|
|
|
|
/** Tests that viewControllerDidAppear starts a trace. */
|
|
- (void)testViewControllerDidAppearStartsATraceForVCWithLoadedView {
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 1);
|
|
NSString *expectedTraceName =
|
|
[FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController];
|
|
XCTAssertNotNil([self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace);
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
XCTAssertEqualObjects(createdTrace.name, expectedTraceName);
|
|
XCTAssertFalse(createdTrace.isCompleteAndValid);
|
|
}
|
|
|
|
/** Tests that the trace is not created when data collection is disabled */
|
|
- (void)testTraceCreationDisabledWhenDataCollectionDisabled {
|
|
@autoreleasepool {
|
|
BOOL dataCollectionEnabled = [FIRPerformance sharedInstance].dataCollectionEnabled;
|
|
[[FIRPerformance sharedInstance] setDataCollectionEnabled:NO];
|
|
UIViewController *newVCInstance =
|
|
FPRCustomViewController(@"MyModule.UIFancyViewController", YES);
|
|
[self.tracker viewControllerDidAppear:newVCInstance];
|
|
|
|
// objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own
|
|
// autorelesepool. Without the autoreleasepool, the ViewController instance is not released
|
|
// in a timely manner and this test becomes flaky.
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:newVCInstance]]
|
|
.trace;
|
|
XCTAssertNil(createdTrace);
|
|
|
|
// Clean up.
|
|
[self.tracker viewControllerDidDisappear:newVCInstance];
|
|
newVCInstance = nil;
|
|
[[FIRPerformance sharedInstance] setDataCollectionEnabled:dataCollectionEnabled];
|
|
}
|
|
}
|
|
|
|
/** Tests that the trace is named correctly in case of Swift classes which are of the format
|
|
* ModuleName.ClassName.
|
|
*/
|
|
- (void)testUnprefixedClassName {
|
|
@autoreleasepool {
|
|
UIViewController *newVCInstance =
|
|
FPRCustomViewController(@"MyModule.UIFancyViewController", YES);
|
|
[self.tracker viewControllerDidAppear:newVCInstance];
|
|
NSString *expectedTraceName = @"_st_UIFancyViewController";
|
|
|
|
// objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own
|
|
// autorelesepool. Without the autoreleasepool, the ViewController instance is not released
|
|
// in a timely manner and this test becomes flaky.
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:newVCInstance]]
|
|
.trace;
|
|
XCTAssertEqualObjects(createdTrace.name, expectedTraceName);
|
|
createdTrace = nil;
|
|
|
|
// Clean up.
|
|
[self.tracker viewControllerDidDisappear:newVCInstance];
|
|
newVCInstance = nil;
|
|
}
|
|
}
|
|
|
|
/** Tests that the module name length is not factored into truncating the screen trace name in case
|
|
* of Swift classes.
|
|
*/
|
|
- (void)testDoesNotTruncateClassNameExtraLongSwiftModuleName {
|
|
NSUInteger valueGreaterThanMaxTraceLength = kFPRMaxNameLength + 10;
|
|
NSMutableString *extraLongModuleName =
|
|
[[NSMutableString alloc] initWithCapacity:valueGreaterThanMaxTraceLength];
|
|
for (int i = 0; i < valueGreaterThanMaxTraceLength; ++i) {
|
|
[extraLongModuleName appendString:@"a"];
|
|
}
|
|
XCTAssertEqual(extraLongModuleName.length, valueGreaterThanMaxTraceLength);
|
|
NSString *swiftClassName =
|
|
[NSString stringWithFormat:@"%@.%@", extraLongModuleName, @"MyViewController"];
|
|
NSString *expectedTraceName = @"_st_MyViewController";
|
|
|
|
@autoreleasepool {
|
|
UIViewController *newVCInstance = FPRCustomViewController(swiftClassName, YES);
|
|
[self.tracker viewControllerDidAppear:newVCInstance];
|
|
|
|
// objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own
|
|
// autorelesepool. Without the autoreleasepool, the ViewController instance is not released
|
|
// in a timely manner and this test becomes flaky.
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:newVCInstance]]
|
|
.trace;
|
|
XCTAssertEqualObjects(createdTrace.name, expectedTraceName);
|
|
createdTrace = nil;
|
|
|
|
// Clean up.
|
|
[self.tracker viewControllerDidDisappear:newVCInstance];
|
|
newVCInstance = nil;
|
|
}
|
|
}
|
|
|
|
/** Tests that if a Swift class name pushes the screen trace name beyond the max trace name length,
|
|
* the screen trace name is truncated.
|
|
*/
|
|
- (void)testTruncatesExtraLongSwiftClassName {
|
|
NSUInteger valueGreaterThanMaxTraceLength = kFPRMaxNameLength + 10;
|
|
NSMutableString *extraLongClassName = [[NSMutableString alloc] init];
|
|
for (int i = 0; i < valueGreaterThanMaxTraceLength; ++i) {
|
|
[extraLongClassName appendString:@"a"];
|
|
}
|
|
XCTAssertEqual(extraLongClassName.length, valueGreaterThanMaxTraceLength);
|
|
NSString *swiftClassName = [NSString stringWithFormat:@"%@.%@", @"MyModule", extraLongClassName];
|
|
|
|
@autoreleasepool {
|
|
UIViewController *newVCInstance = FPRCustomViewController(swiftClassName, YES);
|
|
[self.tracker viewControllerDidAppear:newVCInstance];
|
|
|
|
// objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own
|
|
// autorelesepool. Without the autoreleasepool, the ViewController instance is not released
|
|
// in a timely manner and this test becomes flaky.
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:newVCInstance]]
|
|
.trace;
|
|
XCTAssertEqual(createdTrace.name.length, kFPRMaxNameLength);
|
|
createdTrace = nil;
|
|
|
|
// Clean up.
|
|
[self.tracker viewControllerDidDisappear:newVCInstance];
|
|
newVCInstance = nil;
|
|
}
|
|
}
|
|
|
|
/** Tests that if an ObjC class name pushes the screen trace name beyond the max trace name length,
|
|
* the screen trace name is truncated.
|
|
*/
|
|
- (void)testTruncatesExtraLongObjCClassName {
|
|
NSUInteger valueGreaterThanMaxTraceLength = kFPRMaxNameLength + 10;
|
|
NSMutableString *extraLongClassName = [[NSMutableString alloc] init];
|
|
for (int i = 0; i < valueGreaterThanMaxTraceLength; ++i) {
|
|
[extraLongClassName appendString:@"a"];
|
|
}
|
|
XCTAssertEqual(extraLongClassName.length, valueGreaterThanMaxTraceLength);
|
|
|
|
@autoreleasepool {
|
|
UIViewController *newVCInstance = FPRCustomViewController(extraLongClassName, YES);
|
|
[self.tracker viewControllerDidAppear:newVCInstance];
|
|
|
|
// objectForKey: is always executed on the FPRScreenTraceTracker serial queue, which has its own
|
|
// autorelesepool. Without the autoreleasepool, the ViewController instance is not released
|
|
// in a timely manner and this test becomes flaky.
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:newVCInstance]]
|
|
.trace;
|
|
XCTAssertEqual(createdTrace.name.length, kFPRMaxNameLength);
|
|
createdTrace = nil;
|
|
|
|
// Clean up.
|
|
[self.tracker viewControllerDidDisappear:newVCInstance];
|
|
newVCInstance = nil;
|
|
}
|
|
}
|
|
|
|
/** Tests that a viewController isn't retained by the ScreenTraceTracker. */
|
|
- (void)testViewControllerIsHeldWeaklyByTheScreenTraceTracker {
|
|
__block UIViewController *newVCInstance = nil;
|
|
__weak UIViewController *weakVCReference = nil;
|
|
@autoreleasepool {
|
|
newVCInstance = [[UIViewController alloc] init];
|
|
[newVCInstance view]; // Loads the view so that a screen trace is created for it.
|
|
[self.tracker viewControllerDidAppear:newVCInstance];
|
|
[self.tracker viewControllerDidDisappear:newVCInstance];
|
|
weakVCReference = newVCInstance;
|
|
newVCInstance = nil;
|
|
}
|
|
|
|
XCTAssertNil(weakVCReference);
|
|
}
|
|
|
|
/** Tests that viewControllerDidDisappear stops a trace. */
|
|
- (void)testViewControllerDidDisappearStopsATrace {
|
|
// First screen appears.
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
NSString *expectedTraceName =
|
|
[FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController];
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
XCTAssertNotNil(createdTrace);
|
|
XCTAssertEqualObjects(expectedTraceName, createdTrace.name);
|
|
|
|
// First screen disappears.
|
|
[self.tracker viewControllerDidDisappear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
XCTAssertTrue(createdTrace.isCompleteAndValid);
|
|
}
|
|
|
|
/** Tests that viewControllerDidAppear starts multiple traces if multiple view controllers with the
|
|
* same class appear one after the other.
|
|
*/
|
|
- (void)testViewControllerDidAppearStartsMultipleScreenTracesForSameClassIfNeeded {
|
|
// First screen appears.
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
|
|
// Second screen appears, first screen is still visible.
|
|
UIViewController *testViewController2 = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController2];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
NSString *expectedTraceNameOne =
|
|
[FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController];
|
|
XCTAssertNotNil([self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace);
|
|
FIRTrace *traceForScreenOne =
|
|
[self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
XCTAssertEqualObjects(traceForScreenOne.name, expectedTraceNameOne);
|
|
|
|
NSString *expectedTraceNameTwo =
|
|
[FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController2];
|
|
XCTAssertNotNil([self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController2]]
|
|
.trace);
|
|
FIRTrace *traceForScreenTwo =
|
|
[self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController2]]
|
|
.trace;
|
|
XCTAssertEqualObjects(traceForScreenTwo.name, expectedTraceNameTwo);
|
|
|
|
// Test that they're different instances.
|
|
XCTAssertNotEqual(traceForScreenOne, traceForScreenTwo);
|
|
XCTAssertEqualObjects(traceForScreenOne.name, traceForScreenTwo.name);
|
|
}
|
|
|
|
/** Tests that viewControllerDidAppear starts multiple traces if multiple view controllers with
|
|
* different classes appear one after the other.
|
|
*/
|
|
- (void)testViewControllerDidAppearStartsMultipleScreenTracesForDifferentClassIfNeeded {
|
|
// First screen appears.
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
|
|
// Second screen appears, first screen is still visible.
|
|
UIViewController *testViewController2 = FPRCustomViewController(@"FPRTestViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController2];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
NSString *expectedTraceNameOne =
|
|
[FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController];
|
|
XCTAssertNotNil([self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace);
|
|
FIRTrace *traceForScreenOne =
|
|
[self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
XCTAssertEqualObjects(traceForScreenOne.name, expectedTraceNameOne);
|
|
|
|
NSString *expectedTraceNameTwo =
|
|
[FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController2];
|
|
XCTAssertNotNil([self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController2]]
|
|
.trace);
|
|
FIRTrace *traceForScreenTwo =
|
|
[self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController2]]
|
|
.trace;
|
|
XCTAssertEqualObjects(traceForScreenTwo.name, expectedTraceNameTwo);
|
|
|
|
XCTAssertNotEqual(traceForScreenOne,
|
|
traceForScreenTwo); // Test that they're different instances.
|
|
XCTAssertNotEqualObjects(traceForScreenOne.name, traceForScreenTwo.name);
|
|
}
|
|
|
|
/** Tests that viewControllerDidDisappear stops the correct trace when multiple traces are present.
|
|
*/
|
|
- (void)testViewControllerDidDisappearStopsCorrectTraceWhenMultiplePresent {
|
|
// First screen appears.
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
|
|
// Second screen appears, first screen is still visible.
|
|
UIViewController *testViewController2 = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController2];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 2);
|
|
|
|
FIRTrace *traceForScreenOne =
|
|
[self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
FIRTrace *traceForScreenTwo =
|
|
[self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController2]]
|
|
.trace;
|
|
|
|
XCTAssertFalse(traceForScreenOne.isCompleteAndValid);
|
|
XCTAssertFalse(traceForScreenTwo.isCompleteAndValid);
|
|
|
|
[self.tracker viewControllerDidDisappear:testViewController2];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertFalse(traceForScreenOne.isCompleteAndValid);
|
|
XCTAssertTrue(traceForScreenTwo.isCompleteAndValid);
|
|
}
|
|
|
|
/** Tests that viewControllerDidAppear doesn't start a duplicate trace. */
|
|
- (void)testViewControllerDidAppearIgnoresDuplicateEvent {
|
|
// First screen appears.
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
NSString *expectedTraceName =
|
|
[FPRScreenTraceTrackerTest expectedTraceNameForViewController:testViewController];
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
XCTAssertNotNil(createdTrace);
|
|
XCTAssertEqualObjects(createdTrace.name, expectedTraceName);
|
|
|
|
// Send the same event again.
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 1);
|
|
FIRTrace *activeTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
XCTAssertEqual(createdTrace, activeTrace); // Test that it is the same trace.
|
|
}
|
|
|
|
/** Tests that viewControllerDidAppear gracefully handles a nil viewController. */
|
|
- (void)testViewControllerDidAppearGracefullyHandlesNilViewController {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wnonnull"
|
|
[self.tracker viewControllerDidAppear:nil];
|
|
#pragma clang diagnostic pop
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 0);
|
|
}
|
|
|
|
/** Tests that viewControllerDidDisappear for a viewController that did not appear does nothing. */
|
|
- (void)testViewControllerDidDisappearIgnoresViewControllerThatWasntScreenTraced {
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
UIViewController *testViewController2 = FPRCustomViewController(@"UIViewController", YES);
|
|
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 1);
|
|
|
|
[self.tracker viewControllerDidDisappear:testViewController2];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 1);
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wnonnull"
|
|
[self.tracker viewControllerDidDisappear:nil];
|
|
#pragma clang diagnostic pop
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 1);
|
|
|
|
[self.tracker viewControllerDidDisappear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 0);
|
|
}
|
|
|
|
/** Tests that UIViewControllers are weakly retained in the map table that holds the mapping between
|
|
* them.
|
|
*/
|
|
- (void)testViewControllerIsWeaklyRetained {
|
|
__weak UIViewController *weakVC;
|
|
@autoreleasepool {
|
|
UIViewController *testViewController = [[UIViewController alloc] init];
|
|
weakVC = testViewController;
|
|
id mockTrace = OCMClassMock([FIRTrace class]);
|
|
FPRScreenTraceHolder *holder = [[FPRScreenTraceHolder alloc] init];
|
|
holder.viewController = testViewController;
|
|
holder.trace = mockTrace;
|
|
[self.tracker.activeScreenTraces
|
|
setObject:holder
|
|
forKey:[NSValue valueWithNonretainedObject:testViewController]];
|
|
testViewController = nil;
|
|
}
|
|
|
|
XCTAssertNil(weakVC);
|
|
[self.tracker cleanupStaleTraces];
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 0);
|
|
}
|
|
|
|
/** Tests that a FIRTrace is strongly retained in the map table that holds the mapping between a
|
|
* view controller and its screen trace.
|
|
*/
|
|
- (void)testFIRTraceIsStronglyRetained {
|
|
UIViewController *testViewController = [[UIViewController alloc] init];
|
|
NSString *traceName = @"screenTrace";
|
|
FIRTrace *trace = [[FIRTrace alloc] initInternalTraceWithName:traceName];
|
|
|
|
FPRScreenTraceHolder *holder = [[FPRScreenTraceHolder alloc] init];
|
|
holder.viewController = testViewController;
|
|
holder.trace = trace;
|
|
[self.tracker.activeScreenTraces
|
|
setObject:holder
|
|
forKey:[NSValue valueWithNonretainedObject:testViewController]];
|
|
trace = nil;
|
|
|
|
XCTAssertNotNil([self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace);
|
|
|
|
FIRTrace *returnedTrace =
|
|
[self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
XCTAssertEqualObjects(returnedTrace.name, traceName);
|
|
}
|
|
|
|
/** Tests that a screen trace that doesn't collect any data isn't sent. */
|
|
- (void)testTraceWithNoCountersIsNotSent {
|
|
id mockTrace = OCMClassMock([FIRTrace class]);
|
|
UIViewController *testViewController = [[UIViewController alloc] init];
|
|
FPRScreenTraceHolder *holder = [[FPRScreenTraceHolder alloc] init];
|
|
holder.viewController = testViewController;
|
|
holder.trace = mockTrace;
|
|
[self.tracker.activeScreenTraces
|
|
setObject:holder
|
|
forKey:[NSValue valueWithNonretainedObject:testViewController]];
|
|
|
|
OCMExpect([mockTrace cancel]);
|
|
[[mockTrace reject] stop];
|
|
|
|
[self.tracker viewControllerDidDisappear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
OCMVerifyAll(mockTrace);
|
|
}
|
|
|
|
/** Test that all active traces are stopped when the app resigns active status. */
|
|
- (void)testWillAppResignActiveStopsAllActiveTraces {
|
|
// First screen appears.
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
|
|
// Second screen appears.
|
|
UIViewController *testViewController2 = FPRCustomViewController(@"FPRTestViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController2];
|
|
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
NSValue *keyOne = [NSValue valueWithNonretainedObject:testViewController];
|
|
NSValue *keyTwo = [NSValue valueWithNonretainedObject:testViewController2];
|
|
FIRTrace *traceScreenOne = [self.tracker.activeScreenTraces objectForKey:keyOne].trace;
|
|
FIRTrace *traceScreenTwo = [self.tracker.activeScreenTraces objectForKey:keyTwo].trace;
|
|
|
|
XCTAssertNotNil(traceScreenOne);
|
|
XCTAssertNotNil(traceScreenTwo);
|
|
|
|
// App is backgrounded.
|
|
NSNotification *appWillResignActiveNSNotification =
|
|
[NSNotification notificationWithName:UIApplicationWillResignActiveNotification object:nil];
|
|
[self.tracker appWillResignActiveNotification:appWillResignActiveNSNotification];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertTrue(traceScreenOne.isCompleteAndValid);
|
|
XCTAssertTrue(traceScreenTwo.isCompleteAndValid);
|
|
}
|
|
|
|
/** Test that viewController refs are weakly saved for future use when the app resigns active
|
|
* status.
|
|
*/
|
|
- (void)disabled_testWillAppResignActiveWeaklySavesAllVisibleViewControllers {
|
|
// Screen appears.
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
// App is backgrounded.
|
|
NSNotification *appWillResignActiveNSNotification =
|
|
[NSNotification notificationWithName:UIApplicationWillResignActiveNotification object:nil];
|
|
[self.tracker appWillResignActiveNotification:appWillResignActiveNSNotification];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual(self.tracker.previouslyVisibleViewControllers.count, 1);
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 0);
|
|
|
|
__weak id weakTestViewController = testViewController;
|
|
testViewController = nil;
|
|
|
|
// The blocks retain the view controllers and it sometimes takes some time to release them.
|
|
// This is in place to prevent test flakiness. Autoreleasepools do not work in this case.
|
|
while (weakTestViewController) {
|
|
continue;
|
|
}
|
|
|
|
XCTAssertNil([self.tracker.previouslyVisibleViewControllers pointerAtIndex:0]);
|
|
}
|
|
|
|
/** Tests that new traces are started with the screens that are currently visible after the app
|
|
* regains active status.
|
|
*/
|
|
- (void)testAppDidBecomeActiveWillRestoreTracesOfVisibleScreens {
|
|
// Simulate state where two screen traces were previously active.
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
UIViewController *testViewController2 = FPRCustomViewController(@"FPRTestViewController", YES);
|
|
self.tracker.previouslyVisibleViewControllers = [NSPointerArray weakObjectsPointerArray];
|
|
[self.tracker.previouslyVisibleViewControllers addPointer:(__bridge void *)testViewController];
|
|
[self.tracker.previouslyVisibleViewControllers addPointer:(__bridge void *)testViewController2];
|
|
|
|
// App becomes active.
|
|
NSNotification *appDidBecomeActiveNSNotification =
|
|
[NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification object:nil];
|
|
[self.tracker appDidBecomeActiveNotification:appDidBecomeActiveNSNotification];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 2);
|
|
XCTAssertNil(self.tracker.previouslyVisibleViewControllers);
|
|
XCTAssertNotNil([self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace);
|
|
XCTAssertNotNil([self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController2]]
|
|
.trace);
|
|
}
|
|
|
|
/** Tests that if one of the previously visible ViewControllers is deallocated, a new trace isn't
|
|
* started for it, and the app doesn't crash. */
|
|
- (void)testAppDidBecomeActiveWillNotRestoreTracesOfNilledViewControllers {
|
|
// Simulate state where two screen traces were previously active.
|
|
UIViewController *testViewController = [[UIViewController alloc] init];
|
|
[testViewController view]; // Loads the view so that a screen trace is created for it.
|
|
|
|
__weak UIViewController *weakVC2;
|
|
@autoreleasepool {
|
|
UIViewController *testViewController2 = [[UIViewController alloc] init];
|
|
[testViewController2 view]; // Loads the view so that a screen trace is created for it.
|
|
|
|
self.tracker.previouslyVisibleViewControllers = [NSPointerArray weakObjectsPointerArray];
|
|
[self.tracker.previouslyVisibleViewControllers addPointer:(__bridge void *)testViewController];
|
|
[self.tracker.previouslyVisibleViewControllers addPointer:(__bridge void *)testViewController2];
|
|
|
|
// UIKit deallocates one of the ViewControllers that was previously visible.
|
|
weakVC2 = testViewController2;
|
|
testViewController2 = nil;
|
|
}
|
|
|
|
// The blocks retain the view controllers and it sometimes takes some time to release them.
|
|
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:5.0];
|
|
while (weakVC2 && [timeoutDate timeIntervalSinceNow] > 0) {
|
|
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
|
|
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
|
}
|
|
|
|
// App becomes active.
|
|
NSNotification *appDidBecomeActiveNSNotification =
|
|
[NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification object:nil];
|
|
[self.tracker appDidBecomeActiveNotification:appDidBecomeActiveNSNotification];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertNil(self.tracker.previouslyVisibleViewControllers);
|
|
XCTAssertNotNil([self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace);
|
|
[self.tracker cleanupStaleTraces];
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 1);
|
|
}
|
|
|
|
/** Tests that if consecutive frames take more time to render than the slow frames threshold, the
|
|
* slow frame counter of the screen trace tracker is incremented.
|
|
*/
|
|
- (void)testSlowFrameIsRecorded {
|
|
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
|
|
CFAbsoluteTime secondFrameRenderTimestamp =
|
|
firstFrameRenderTimestamp + kFPRSlowFrameThreshold + 0.005; // Buffer for float comparison.
|
|
|
|
id displayLinkMock = OCMClassMock([CADisplayLink class]);
|
|
[self.tracker.displayLink invalidate];
|
|
self.tracker.displayLink = displayLinkMock;
|
|
|
|
// Set/Reset the previousFrameTimestamp if it has been set by a previous test.
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
|
|
int64_t newSlowFramesCount = self.tracker.slowFramesCount;
|
|
XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount + 1);
|
|
}
|
|
|
|
/** Tests that the slow and frozen frame counter is not incremented in the case of a good frame. */
|
|
- (void)testSlowAndFrozenFrameIsNotRecordedInCaseOfGoodFrame {
|
|
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
|
|
// Use a frame duration that's clearly below any reasonable threshold (even for 120 FPS devices).
|
|
// For 120 FPS: threshold = 1/120 = 0.008333, with epsilon = 0.001, so slow if > 0.009333.
|
|
// Using 0.005 ensures it's a good frame on all devices.
|
|
CFAbsoluteTime secondFrameRenderTimestamp =
|
|
firstFrameRenderTimestamp + 0.005; // Good frame (5ms, well below any threshold).
|
|
|
|
id displayLinkMock = OCMClassMock([CADisplayLink class]);
|
|
[self.tracker.displayLink invalidate];
|
|
self.tracker.displayLink = displayLinkMock;
|
|
|
|
// Set/Reset the previousFrameTimestamp if it has been set by a previous test.
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount;
|
|
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
|
|
int64_t newSlowFramesCount = self.tracker.slowFramesCount;
|
|
int64_t newFrozenFramesCount = self.tracker.frozenFramesCount;
|
|
|
|
XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount);
|
|
XCTAssertEqual(newFrozenFramesCount, initialFrozenFramesCount);
|
|
}
|
|
|
|
/* Tests that the frozen frame counter is not incremented in case of a slow frame. */
|
|
- (void)testFrozenFrameIsNotRecordedInCaseOfSlowFrame {
|
|
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
|
|
CFAbsoluteTime secondFrameRenderTimestamp =
|
|
firstFrameRenderTimestamp + kFPRSlowFrameThreshold + 0.005; // Slow frame.
|
|
|
|
id displayLinkMock = OCMClassMock([CADisplayLink class]);
|
|
[self.tracker.displayLink invalidate];
|
|
self.tracker.displayLink = displayLinkMock;
|
|
|
|
// Set/Reset the previousFrameTimestamp if it has been set by a previous test.
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
|
|
int64_t newFrozenFramesCount = self.tracker.frozenFramesCount;
|
|
XCTAssertEqual(newFrozenFramesCount, initialFrozenFramesCount);
|
|
}
|
|
|
|
/** Tests that the total frames counter is incremented in the case of good, slow and frozen
|
|
* frames.
|
|
*/
|
|
- (void)testTotalFramesAreAlwaysRecorded {
|
|
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
|
|
CFAbsoluteTime secondFrameRenderTimestamp =
|
|
firstFrameRenderTimestamp + kFPRSlowFrameThreshold - 0.005; // Good frame.
|
|
CFAbsoluteTime thirdFrameRenderTimestamp =
|
|
secondFrameRenderTimestamp + kFPRSlowFrameThreshold + 0.005; // Slow frame.
|
|
CFAbsoluteTime fourthFrameRenderTimestamp =
|
|
thirdFrameRenderTimestamp + kFPRFrozenFrameThreshold + 0.005; // Frozen frame.
|
|
|
|
id displayLinkMock = OCMClassMock([CADisplayLink class]);
|
|
[self.tracker.displayLink invalidate];
|
|
self.tracker.displayLink = displayLinkMock;
|
|
|
|
// Set/Reset the previousFrameTimestamp if it has been set by a previous test.
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
int64_t initialTotalFramesCount = self.tracker.totalFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
int64_t newTotalFramesCount = self.tracker.totalFramesCount;
|
|
XCTAssertEqual(newTotalFramesCount, initialTotalFramesCount + 1);
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
newTotalFramesCount = self.tracker.totalFramesCount;
|
|
XCTAssertEqual(newTotalFramesCount, initialTotalFramesCount + 2);
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(fourthFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
newTotalFramesCount = self.tracker.totalFramesCount;
|
|
XCTAssertEqual(newTotalFramesCount, initialTotalFramesCount + 3);
|
|
}
|
|
|
|
/** Tests that if consecutive frames take more time to render than the frozen frames threshold, the
|
|
* frozen frame counter and slow frame counter of the screen trace tracker is incremented.
|
|
*/
|
|
- (void)testFrozenFrameAndSlowFrameIsRecorded {
|
|
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
|
|
CFAbsoluteTime secondFrameRenderTimestamp =
|
|
firstFrameRenderTimestamp + kFPRFrozenFrameThreshold + 0.005; // Buffer for float comparison.
|
|
|
|
id displayLinkMock = OCMClassMock([CADisplayLink class]);
|
|
[self.tracker.displayLink invalidate];
|
|
self.tracker.displayLink = displayLinkMock;
|
|
|
|
// Set/Reset the previousFrameTimestamp if it has been set by a previous test.
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
|
|
int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
int64_t newSlowFramesCount = self.tracker.slowFramesCount;
|
|
int64_t newFrozenFramesCount = self.tracker.frozenFramesCount;
|
|
|
|
XCTAssertEqual(newFrozenFramesCount, initialFrozenFramesCount + 1);
|
|
XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount + 1);
|
|
}
|
|
|
|
/** Tests that the correct number of slow, frozen and total frames are recorded when all 3 are
|
|
* present.
|
|
*/
|
|
- (void)testTraceHasCorrectFrozenSlowAndTotalFrameMetricsWhenThoseFramesAreRecorded {
|
|
int64_t initialTotalFramesCount = self.tracker.totalFramesCount;
|
|
int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount;
|
|
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
|
|
|
|
int64_t expectedTotalFramesOnTrace = 5;
|
|
int64_t expectedSlowFramesOnTrace = 3;
|
|
int64_t expectedFrozenFramesOnTrace = 1;
|
|
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
|
|
self.tracker.totalFramesCount = initialTotalFramesCount + expectedTotalFramesOnTrace;
|
|
self.tracker.slowFramesCount = initialSlowFramesCount + expectedSlowFramesOnTrace;
|
|
self.tracker.frozenFramesCount = initialFrozenFramesCount + expectedFrozenFramesOnTrace;
|
|
|
|
[self.tracker viewControllerDidDisappear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual([createdTrace valueForIntMetric:kFPRSlowFrameCounterName],
|
|
expectedSlowFramesOnTrace);
|
|
XCTAssertEqual([createdTrace valueForIntMetric:kFPRFrozenFrameCounterName],
|
|
expectedFrozenFramesOnTrace);
|
|
XCTAssertEqual([createdTrace valueForIntMetric:kFPRTotalFramesCounterName],
|
|
expectedTotalFramesOnTrace);
|
|
}
|
|
|
|
/** Tests that if just total and slow frame and no frozen frames are recorded, then the frozen
|
|
* frames metric is not present on the trace. */
|
|
- (void)testTraceHasJustSlowAndTotalFrameMetricsWhenNoFrozenFramesAreRecorded {
|
|
int64_t initialTotalFramesCount = self.tracker.totalFramesCount;
|
|
int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount;
|
|
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
|
|
|
|
int64_t expectedTotalFramesOnTrace = 5;
|
|
int64_t expectedSlowFramesOnTrace = 3;
|
|
int64_t expectedFrozenFramesOnTrace = 0;
|
|
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
|
|
self.tracker.totalFramesCount = initialTotalFramesCount + expectedTotalFramesOnTrace;
|
|
self.tracker.slowFramesCount = initialSlowFramesCount + expectedSlowFramesOnTrace;
|
|
self.tracker.frozenFramesCount = initialFrozenFramesCount + expectedFrozenFramesOnTrace;
|
|
|
|
[self.tracker viewControllerDidDisappear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual([createdTrace valueForIntMetric:kFPRSlowFrameCounterName],
|
|
expectedSlowFramesOnTrace);
|
|
XCTAssertEqual([createdTrace valueForIntMetric:kFPRTotalFramesCounterName],
|
|
expectedTotalFramesOnTrace);
|
|
XCTAssertNil(createdTrace.counters[kFPRFrozenFrameCounterName]);
|
|
XCTAssertEqual(createdTrace.counters.count, 2);
|
|
}
|
|
|
|
/** Tests that when no frozen or slow frames are recorded, the trace only has the total frames
|
|
* counter.
|
|
*/
|
|
- (void)testTraceHasJustTotalFrameMetricsWhenNoFrozenOrSlowFramesAreRecorded {
|
|
int64_t initialTotalFramesCount = self.tracker.totalFramesCount;
|
|
int64_t initialFrozenFramesCount = self.tracker.frozenFramesCount;
|
|
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
|
|
|
|
int64_t expectedTotalFramesOnTrace = 5;
|
|
int64_t expectedSlowFramesOnTrace = 0;
|
|
int64_t expectedFrozenFramesOnTrace = 0;
|
|
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
|
|
self.tracker.totalFramesCount = initialTotalFramesCount + expectedTotalFramesOnTrace;
|
|
self.tracker.slowFramesCount = initialSlowFramesCount + expectedSlowFramesOnTrace;
|
|
self.tracker.frozenFramesCount = initialFrozenFramesCount + expectedFrozenFramesOnTrace;
|
|
|
|
[self.tracker viewControllerDidDisappear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual([createdTrace valueForIntMetric:kFPRTotalFramesCounterName],
|
|
expectedTotalFramesOnTrace);
|
|
XCTAssertNil(createdTrace.counters[kFPRSlowFrameCounterName]);
|
|
XCTAssertNil(createdTrace.counters[kFPRFrozenFrameCounterName]);
|
|
XCTAssertEqual(createdTrace.counters.count, 1);
|
|
}
|
|
|
|
/** Tests that if no frames are recorded between a trace being started and stopped, it doesn't have
|
|
* any metrics associated with it.
|
|
*/
|
|
- (void)testTraceHasNoMetricsWhenNoFramesAreRecorded {
|
|
UIViewController *testViewController = FPRCustomViewController(@"UIViewController", YES);
|
|
[self.tracker viewControllerDidAppear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
FIRTrace *createdTrace = [self.tracker.activeScreenTraces
|
|
objectForKey:[NSValue valueWithNonretainedObject:testViewController]]
|
|
.trace;
|
|
|
|
[self.tracker viewControllerDidDisappear:testViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual(createdTrace.counters.count, 0);
|
|
}
|
|
|
|
/** Tests that screen traces are NOT created for container view controllers. */
|
|
- (void)testScreenTracesAreNotCreatedForContainerViewControllers {
|
|
UINavigationController *testNavigationController =
|
|
(UINavigationController *)FPRCustomViewController(@"UINavigationController", YES);
|
|
|
|
UITabBarController *testTabBarController =
|
|
(UITabBarController *)FPRCustomViewController(@"UITabBarController", YES);
|
|
|
|
UISplitViewController *testSplitViewController =
|
|
(UISplitViewController *)FPRCustomViewController(@"UISplitViewController", YES);
|
|
|
|
UIPageViewController *testPageViewController =
|
|
(UIPageViewController *)FPRCustomViewController(@"UIPageViewController", YES);
|
|
|
|
[self.tracker viewControllerDidAppear:testNavigationController];
|
|
[self.tracker viewControllerDidAppear:testTabBarController];
|
|
[self.tracker viewControllerDidAppear:testSplitViewController];
|
|
[self.tracker viewControllerDidAppear:testPageViewController];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 0);
|
|
}
|
|
|
|
/** Tests that screen traces are created for canonical container view controller subclasses. */
|
|
- (void)testScreenTracesAreCreatedForContainerViewControllerSubclasses {
|
|
FPRTestNavigationViewController *testNavigationControllerSubclass =
|
|
(FPRTestNavigationViewController *)FPRCustomViewController(@"FPRTestNavigationViewController",
|
|
YES);
|
|
|
|
FPRTestTabBarController *testTabBarControllerSubclass =
|
|
(FPRTestTabBarController *)FPRCustomViewController(@"FPRTestTabBarController", YES);
|
|
|
|
FPRTestSplitViewController *testSplitViewControllerSubclass =
|
|
(FPRTestSplitViewController *)FPRCustomViewController(@"FPRTestSplitViewController", YES);
|
|
|
|
FPRTestPageViewController *testPageViewControllerSubclass =
|
|
(FPRTestPageViewController *)FPRCustomViewController(@"FPRTestPageViewController", YES);
|
|
|
|
[self.tracker viewControllerDidAppear:testNavigationControllerSubclass];
|
|
[self.tracker viewControllerDidAppear:testTabBarControllerSubclass];
|
|
[self.tracker viewControllerDidAppear:testSplitViewControllerSubclass];
|
|
[self.tracker viewControllerDidAppear:testPageViewControllerSubclass];
|
|
dispatch_group_wait(self.dispatchGroupToWaitOn, DISPATCH_TIME_FOREVER);
|
|
|
|
XCTAssertEqual(self.tracker.activeScreenTraces.count, 4);
|
|
}
|
|
|
|
#pragma mark - Dynamic FPS Tests
|
|
|
|
#if TARGET_OS_TV
|
|
|
|
/** Helper method to create a test tracker with updated cached budget.
|
|
*
|
|
* @return A configured FPRScreenTraceTracker instance.
|
|
*/
|
|
- (FPRScreenTraceTracker *)createTestTrackerWithUpdatedFPS {
|
|
FPRScreenTraceTracker *testTracker = [[FPRScreenTraceTracker alloc] init];
|
|
testTracker.displayLink.paused = YES;
|
|
// Tests run on main thread, so call updateCachedSlowBudget directly.
|
|
[testTracker updateCachedSlowBudget];
|
|
return testTracker;
|
|
}
|
|
|
|
/** Tests that slow frames are correctly detected with a custom maxFPS value on tvOS.
|
|
* This test stubs UIScreen.maximumFramesPerSecond to 50 FPS and verifies that frames
|
|
* at ~25ms (slow) and ~19ms (not slow) are correctly classified.
|
|
*/
|
|
- (void)testSlowFrameIsRecordedWithCustomMaxFPSOnTvOS {
|
|
// At 50 FPS, slow budget = 1.0/50 = 0.02 seconds = 20ms.
|
|
[self withStubbedMaxFPS:50
|
|
performBlock:^{
|
|
// Verify the stub is working before creating the tracker.
|
|
UIScreen *mainScreen = [UIScreen mainScreen];
|
|
XCTAssertEqual(mainScreen.maximumFramesPerSecond, 50, @"Stub should return 50 FPS");
|
|
|
|
FPRScreenTraceTracker *testTracker = [self createTestTrackerWithUpdatedFPS];
|
|
|
|
// Verify the stub is still working after tracker creation.
|
|
XCTAssertEqual([UIScreen mainScreen].maximumFramesPerSecond, 50,
|
|
@"Stub should still return 50 FPS after tracker creation");
|
|
// At 50 FPS, effectiveFPS = 50 (not 60, so no 59 conversion), threshold = 1/50 =
|
|
// 0.02 = 20ms Force update to ensure budget is set correctly.
|
|
[testTracker updateCachedSlowBudget];
|
|
|
|
// At 50 FPS, slow budget = 20ms. With epsilon (0.001), frames > 20.001ms are slow.
|
|
// Test with a frame clearly above threshold to ensure it's marked as slow.
|
|
// Use 25ms to be well above the 20ms threshold.
|
|
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
|
|
CFAbsoluteTime secondFrameRenderTimestamp =
|
|
firstFrameRenderTimestamp + 0.025; // 25ms, clearly slow
|
|
|
|
id displayLinkMock = OCMClassMock([CADisplayLink class]);
|
|
[testTracker.displayLink invalidate];
|
|
testTracker.displayLink = displayLinkMock;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
int64_t initialSlowFramesCount = testTracker.slowFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
|
|
int64_t newSlowFramesCount = testTracker.slowFramesCount;
|
|
XCTAssertEqual(newSlowFramesCount, initialSlowFramesCount + 1,
|
|
@"Frame at 25ms should be marked as slow at 50 FPS (20ms threshold)");
|
|
|
|
// Test with 19ms frame (should NOT be slow).
|
|
CFAbsoluteTime thirdFrameRenderTimestamp =
|
|
secondFrameRenderTimestamp + 0.019; // 19ms, not slow
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
|
|
int64_t finalSlowFramesCount = testTracker.slowFramesCount;
|
|
XCTAssertEqual(
|
|
finalSlowFramesCount, newSlowFramesCount,
|
|
@"Frame at 19ms should NOT be marked as slow at 50 FPS (20ms threshold)");
|
|
}];
|
|
}
|
|
|
|
/** Tests that the epsilon value correctly handles edge cases around 59.94 vs 60 Hz displays on
|
|
* tvOS. Frames right at the threshold should not be miscounted due to floating point precision.
|
|
*/
|
|
- (void)testSlowFrameRate_isHandled_inEdgeCases {
|
|
[self withStubbedMaxFPS:60
|
|
performBlock:^{
|
|
FPRScreenTraceTracker *testTracker = [self createTestTrackerWithUpdatedFPS];
|
|
|
|
// Verify the stub is working - UIScreen should return 60 FPS.
|
|
UIScreen *mainScreen = [UIScreen mainScreen];
|
|
XCTAssertEqual(mainScreen.maximumFramesPerSecond, 60, @"Stub should return 60 FPS");
|
|
|
|
// At 60 FPS, slow budget = 1.0/60 = 0.016666... seconds.
|
|
// With epsilon (0.001), frames > 0.017666... are slow.
|
|
// Test with frame exactly at threshold (should NOT be slow due to epsilon).
|
|
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
|
|
CFTimeInterval exactThreshold = 1.0 / 60.0; // Exactly 1/60 second
|
|
CFAbsoluteTime secondFrameRenderTimestamp =
|
|
firstFrameRenderTimestamp + exactThreshold;
|
|
|
|
id displayLinkMock = OCMClassMock([CADisplayLink class]);
|
|
[testTracker.displayLink invalidate];
|
|
testTracker.displayLink = displayLinkMock;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
int64_t initialSlowFramesCount = testTracker.slowFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
|
|
int64_t newSlowFramesCount = testTracker.slowFramesCount;
|
|
XCTAssertEqual(
|
|
newSlowFramesCount, initialSlowFramesCount,
|
|
@"Frame exactly at threshold should NOT be marked as slow due to epsilon");
|
|
|
|
// Test with frame just above threshold + epsilon (should be slow).
|
|
// Use a value clearly above threshold + epsilon (0.001) to account for floating
|
|
// point precision. We use 0.002 above threshold to ensure it's clearly above the
|
|
// epsilon threshold.
|
|
CFTimeInterval justAboveThreshold =
|
|
exactThreshold + 0.001 + 0.001; // 0.002 above threshold (epsilon is 0.001)
|
|
CFAbsoluteTime thirdFrameRenderTimestamp =
|
|
secondFrameRenderTimestamp + justAboveThreshold;
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(thirdFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
|
|
int64_t finalSlowFramesCount = testTracker.slowFramesCount;
|
|
XCTAssertEqual(finalSlowFramesCount, newSlowFramesCount + 1,
|
|
@"Frame just above threshold + epsilon should be marked as slow");
|
|
}];
|
|
}
|
|
|
|
/** Tests that the slow budget is recomputed when UIScreenModeDidChangeNotification is posted on
|
|
* tvOS. This verifies that the tracker adapts to display mode changes that affect refresh rate.
|
|
*/
|
|
- (void)testScreenModeChangeUpdatesSlowBudgetOnTvOS {
|
|
// Swizzle UIScreen.maximumFramesPerSecond to return 60 FPS initially, then 50 FPS.
|
|
Method originalMethod =
|
|
class_getInstanceMethod([UIScreen class], @selector(maximumFramesPerSecond));
|
|
IMP originalIMP = method_getImplementation(originalMethod);
|
|
|
|
__block NSInteger stubbedMaxFPS = 60;
|
|
NSInteger (^stubBlock)(id) = ^NSInteger(id self) {
|
|
return stubbedMaxFPS;
|
|
};
|
|
IMP stubIMP = imp_implementationWithBlock(stubBlock);
|
|
method_setImplementation(originalMethod, stubIMP);
|
|
|
|
@try {
|
|
FPRScreenTraceTracker *testTracker = [self createTestTrackerWithUpdatedFPS];
|
|
|
|
// Verify initial behavior: at 60 FPS, slow budget = ~16.67ms.
|
|
// An 18ms frame should be slow at 60 FPS.
|
|
CFAbsoluteTime firstFrameRenderTimestamp = 1.0;
|
|
CFAbsoluteTime secondFrameRenderTimestamp = firstFrameRenderTimestamp + 0.018; // 18ms
|
|
|
|
id displayLinkMock = OCMClassMock([CADisplayLink class]);
|
|
[testTracker.displayLink invalidate];
|
|
testTracker.displayLink = displayLinkMock;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
int64_t initialSlowFramesCount = testTracker.slowFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
|
|
int64_t slowFramesAfter18ms = testTracker.slowFramesCount;
|
|
// At 60 FPS (~16.67ms threshold), 18ms frame should be slow.
|
|
XCTAssertEqual(slowFramesAfter18ms, initialSlowFramesCount + 1,
|
|
@"At 60 FPS, 18ms frame should be slow (threshold is ~16.67ms)");
|
|
|
|
// Change the stubbed maxFPS to 50 FPS.
|
|
stubbedMaxFPS = 50;
|
|
|
|
// Post the notification to trigger recomputation.
|
|
NSNotification *modeChangeNotification =
|
|
[NSNotification notificationWithName:UIScreenModeDidChangeNotification object:nil];
|
|
[testTracker screenModeDidChangeNotification:modeChangeNotification];
|
|
|
|
// Wait for the async update to complete. Since screenModeDidChangeNotification dispatches
|
|
// async to main queue, and tests run on main thread, we need to run the run loop to process it.
|
|
// Run the run loop multiple times to ensure the async dispatch completes.
|
|
NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:0.5];
|
|
while ([timeout timeIntervalSinceNow] > 0) {
|
|
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
|
|
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
|
|
}
|
|
|
|
// Also directly update to ensure it's set (in case async didn't complete).
|
|
[testTracker updateCachedSlowBudget];
|
|
|
|
// Verify the stub is now returning 50 FPS.
|
|
UIScreen *mainScreen = [UIScreen mainScreen];
|
|
XCTAssertEqual(mainScreen.maximumFramesPerSecond, 50,
|
|
@"Stub should now return 50 FPS after change");
|
|
|
|
// Verify the new budget is used: at 50 FPS, slow budget = 20ms.
|
|
// An 18ms frame should NOT be slow at 50 FPS (it's below the 20ms threshold).
|
|
testTracker.slowFramesCount = 0;
|
|
firstFrameRenderTimestamp = 2.0;
|
|
secondFrameRenderTimestamp = firstFrameRenderTimestamp + 0.018; // 18ms
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
initialSlowFramesCount = testTracker.slowFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameRenderTimestamp);
|
|
[testTracker displayLinkStep];
|
|
|
|
int64_t slowFramesAfterModeChange = testTracker.slowFramesCount;
|
|
// At 50 FPS (20ms threshold), 18ms frame should NOT be slow.
|
|
XCTAssertEqual(
|
|
slowFramesAfterModeChange, initialSlowFramesCount,
|
|
@"After mode change to 50 FPS, 18ms frame should NOT be slow (threshold is 20ms)");
|
|
} @finally {
|
|
// Restore original implementation.
|
|
method_setImplementation(originalMethod, originalIMP);
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
/** Tests that non tvOS devices (including ProMotion devices) displays do not use the dynamic frame
|
|
* rate. */
|
|
- (void)testNonTVOSDevicesDontUseMaximumFramesPerSecond {
|
|
[self withStubbedMaxFPS:120
|
|
performBlock:^{
|
|
[self.tracker updateCachedSlowBudget];
|
|
|
|
id displayLinkMock = OCMClassMock([CADisplayLink class]);
|
|
[self.tracker.displayLink invalidate];
|
|
self.tracker.displayLink = displayLinkMock;
|
|
|
|
CFAbsoluteTime firstFrameTimestamp = 1.0;
|
|
// Standard 60 FPS frame duration is ~16.67ms (1.0 / 60.0)
|
|
CFAbsoluteTime secondFrameTimestamp = firstFrameTimestamp + (1.0 / 60.0);
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(firstFrameTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
int64_t initialSlowFramesCount = self.tracker.slowFramesCount;
|
|
|
|
OCMExpect([displayLinkMock timestamp]).andReturn(secondFrameTimestamp);
|
|
[self.tracker displayLinkStep];
|
|
|
|
int64_t slowFramesCount = self.tracker.slowFramesCount;
|
|
XCTAssertEqual(slowFramesCount, initialSlowFramesCount,
|
|
@"Standard 60 FPS frames should NOT be marked as slow on ProMotion "
|
|
@"(120 FPS) devices on iOS");
|
|
}];
|
|
}
|
|
|
|
#endif
|
|
|
|
#pragma mark - Helper methods
|
|
|
|
/** Helper method to swizzle UIScreen.maximumFramesPerSecond for testing.
|
|
*
|
|
* @param fps The FPS value to stub.
|
|
* @param block The block to execute with the stubbed FPS.
|
|
*/
|
|
- (void)withStubbedMaxFPS:(NSInteger)fps performBlock:(void (^)(void))block {
|
|
Method originalMethod =
|
|
class_getInstanceMethod([UIScreen class], @selector(maximumFramesPerSecond));
|
|
IMP originalIMP = method_getImplementation(originalMethod);
|
|
|
|
NSInteger (^stubBlock)(id) = ^NSInteger(id self) {
|
|
return fps;
|
|
};
|
|
IMP stubIMP = imp_implementationWithBlock(stubBlock);
|
|
method_setImplementation(originalMethod, stubIMP);
|
|
|
|
@try {
|
|
block();
|
|
} @finally {
|
|
method_setImplementation(originalMethod, originalIMP);
|
|
}
|
|
}
|
|
|
|
+ (NSString *)expectedTraceNameForViewController:(UIViewController *)viewController {
|
|
return [@"_st_" stringByAppendingString:NSStringFromClass([viewController class])];
|
|
}
|
|
|
|
- (void)testScreenTraceTrackerStress {
|
|
int iterations = 100;
|
|
int numObjects = 10;
|
|
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@"Stress test finished"];
|
|
|
|
dispatch_queue_t queue =
|
|
dispatch_queue_create("com.google.perf.stress", DISPATCH_QUEUE_CONCURRENT);
|
|
|
|
dispatch_group_t group = dispatch_group_create();
|
|
|
|
NSMutableArray<UIViewController *> *objects = [[NSMutableArray alloc] init];
|
|
for (int i = 0; i < numObjects; i++) {
|
|
[objects addObject:[[UIViewController alloc] init]];
|
|
}
|
|
|
|
// Thread 1: Continuously call viewControllerDidAppear
|
|
for (int t = 0; t < 3; t++) {
|
|
dispatch_group_async(group, queue, ^{
|
|
for (int i = 0; i < iterations; i++) {
|
|
int idx = i % numObjects;
|
|
UIViewController *obj;
|
|
@synchronized(objects) {
|
|
obj = objects[idx];
|
|
}
|
|
[self.tracker viewControllerDidAppear:obj];
|
|
}
|
|
});
|
|
}
|
|
|
|
// Thread 2: Continuously call viewControllerDidDisappear
|
|
for (int t = 0; t < 3; t++) {
|
|
dispatch_group_async(group, queue, ^{
|
|
for (int i = 0; i < iterations; i++) {
|
|
int idx = i % numObjects;
|
|
UIViewController *obj;
|
|
@synchronized(objects) {
|
|
obj = objects[idx];
|
|
}
|
|
[self.tracker viewControllerDidDisappear:obj];
|
|
}
|
|
});
|
|
}
|
|
|
|
// Thread 3: Continuously replace objects to trigger deallocation
|
|
for (int t = 0; t < 2; t++) {
|
|
dispatch_group_async(group, queue, ^{
|
|
for (int i = 0; i < iterations; i++) {
|
|
int idx = i % numObjects;
|
|
UIViewController *newObj = [[UIViewController alloc] init];
|
|
@synchronized(objects) {
|
|
objects[idx] = newObj;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
|
[expectation fulfill];
|
|
});
|
|
|
|
[self waitForExpectationsWithTimeout:30 handler:nil];
|
|
}
|
|
|
|
@end
|