AutorÃa | Ultima modificación | Ver Log |
// 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.h"#import "FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker+Private.h"#import <Foundation/Foundation.h>#import <UIKit/UIKit.h>#import "FirebasePerformance/Sources/Common/FPRDiagnostics.h"NSString *const kFPRPrefixForScreenTraceName = @"_st_";NSString *const kFPRFrozenFrameCounterName = @"_fr_fzn";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.// TODO(b/73498642): Make these configurable.CFTimeInterval const kFPRSlowFrameThreshold = 1.0 / 59.0; // Anything less than 59 FPS is slow.CFTimeInterval const kFPRFrozenFrameThreshold = 700.0 / 1000.0;/** Constant that indicates an invalid time. */CFAbsoluteTime const kFPRInvalidTime = -1.0;/** Returns the class name without the prefixed module name present in Swift classes* (e.g. MyModule.MyViewController -> MyViewController).*/static NSString *FPRUnprefixedClassName(Class theClass) {NSString *className = NSStringFromClass(theClass);NSRange periodRange = [className rangeOfString:@"." options:NSBackwardsSearch];if (periodRange.location == NSNotFound) {return className;}return periodRange.location < className.length - 1? [className substringFromIndex:periodRange.location + 1]: className;}/** Returns the name for the screen trace for a given UIViewController. It does the following:* - Removes module name from swift classes - (e.g. MyModule.MyViewController -> MyViewController)* - Prepends "_st_" to the class name* - Truncates the length if it exceeds the maximum trace length.** @param viewController The view controller whose screen trace name we want. Cannot be nil.* @return An NSString containing the trace name, or a string containing an error if the* class was nil.*/static NSString *FPRScreenTraceNameForViewController(UIViewController *viewController) {NSString *unprefixedClassName = FPRUnprefixedClassName([viewController class]);if (unprefixedClassName.length != 0) {NSString *traceName =[NSString stringWithFormat:@"%@%@", kFPRPrefixForScreenTraceName, unprefixedClassName];return traceName.length > kFPRMaxNameLength ? [traceName substringToIndex:kFPRMaxNameLength]: traceName;} else {// This is unlikely, but might happen if there's a regression on iOS where the class name// returned for a non-nil class is nil or empty.return @"_st_ERROR_NIL_CLASS_NAME";}}@implementation FPRScreenTraceTracker {/** Instance variable storing the total frames observed so far. */atomic_int_fast64_t _totalFramesCount;/** Instance variable storing the slow frames observed so far. */atomic_int_fast64_t _slowFramesCount;/** Instance variable storing the frozen frames observed so far. */atomic_int_fast64_t _frozenFramesCount;}@dynamic totalFramesCount;@dynamic frozenFramesCount;@dynamic slowFramesCount;+ (instancetype)sharedInstance {static FPRScreenTraceTracker *instance;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{instance = [[self alloc] init];});return instance;}- (instancetype)init {self = [super init];if (self) {// Weakly retain viewController, use pointer hashing.NSMapTableOptions keyOptions = NSMapTableWeakMemory | NSMapTableObjectPointerPersonality;// Strongly retain the FIRTrace.NSMapTableOptions valueOptions = NSMapTableStrongMemory;_activeScreenTraces = [NSMapTable mapTableWithKeyOptions:keyOptions valueOptions:valueOptions];_previouslyVisibleViewControllers = nil; // Will be set when there is data._screenTraceTrackerSerialQueue =dispatch_queue_create("com.google.FPRScreenTraceTracker", DISPATCH_QUEUE_SERIAL);_screenTraceTrackerDispatchGroup = dispatch_group_create();atomic_store_explicit(&_totalFramesCount, 0, memory_order_relaxed);atomic_store_explicit(&_frozenFramesCount, 0, memory_order_relaxed);atomic_store_explicit(&_slowFramesCount, 0, memory_order_relaxed);_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkStep)];[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];// We don't receive background and foreground events from analytics and so we have to listen to// them ourselves.[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(appDidBecomeActiveNotification:)name:UIApplicationDidBecomeActiveNotificationobject:[UIApplication sharedApplication]];[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(appWillResignActiveNotification:)name:UIApplicationWillResignActiveNotificationobject:[UIApplication sharedApplication]];}return self;}- (void)dealloc {[_displayLink invalidate];[[NSNotificationCenter defaultCenter] removeObserver:selfname:UIApplicationDidBecomeActiveNotificationobject:[UIApplication sharedApplication]];[[NSNotificationCenter defaultCenter] removeObserver:selfname:UIApplicationWillResignActiveNotificationobject:[UIApplication sharedApplication]];}- (void)appDidBecomeActiveNotification:(NSNotification *)notification {// To get the most accurate numbers of total, frozen and slow frames, we need to capture them as// soon as we're notified of an event.int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);int64_t currentFrozenFrames = atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);int64_t currentSlowFrames = atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);dispatch_group_async(self.screenTraceTrackerDispatchGroup, self.screenTraceTrackerSerialQueue, ^{for (id viewController in self.previouslyVisibleViewControllers) {[self startScreenTraceForViewController:viewControllercurrentTotalFrames:currentTotalFramescurrentFrozenFrames:currentFrozenFramescurrentSlowFrames:currentSlowFrames];}self.previouslyVisibleViewControllers = nil;});}- (void)appWillResignActiveNotification:(NSNotification *)notification {// To get the most accurate numbers of total, frozen and slow frames, we need to capture them as// soon as we're notified of an event.int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);int64_t currentFrozenFrames = atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);int64_t currentSlowFrames = atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);dispatch_group_async(self.screenTraceTrackerDispatchGroup, self.screenTraceTrackerSerialQueue, ^{self.previouslyVisibleViewControllers = [NSPointerArray weakObjectsPointerArray];id visibleViewControllersEnumerator = [self.activeScreenTraces keyEnumerator];id visibleViewController = nil;while (visibleViewController = [visibleViewControllersEnumerator nextObject]) {[self.previouslyVisibleViewControllers addPointer:(__bridge void *)(visibleViewController)];}for (id visibleViewController in self.previouslyVisibleViewControllers) {[self stopScreenTraceForViewController:visibleViewControllercurrentTotalFrames:currentTotalFramescurrentFrozenFrames:currentFrozenFramescurrentSlowFrames:currentSlowFrames];}});}#pragma mark - Frozen, slow and good frames- (void)displayLinkStep {static CFAbsoluteTime previousTimestamp = kFPRInvalidTime;CFAbsoluteTime currentTimestamp = self.displayLink.timestamp;RecordFrameType(currentTimestamp, previousTimestamp, &_slowFramesCount, &_frozenFramesCount,&_totalFramesCount);previousTimestamp = currentTimestamp;}/** This function increments the relevant frame counters based on the current and previous* timestamp provided by the displayLink.** @param currentTimestamp The current timestamp of the displayLink.* @param previousTimestamp The previous timestamp of the displayLink.* @param slowFramesCounter The value of the slowFramesCount before this function was called.* @param frozenFramesCounter The value of the frozenFramesCount before this function was called.* @param totalFramesCounter The value of the totalFramesCount before this function was called.*/FOUNDATION_STATIC_INLINEvoid RecordFrameType(CFAbsoluteTime currentTimestamp,CFAbsoluteTime previousTimestamp,atomic_int_fast64_t *slowFramesCounter,atomic_int_fast64_t *frozenFramesCounter,atomic_int_fast64_t *totalFramesCounter) {CFTimeInterval frameDuration = currentTimestamp - previousTimestamp;if (previousTimestamp == kFPRInvalidTime) {return;}if (frameDuration > kFPRSlowFrameThreshold) {atomic_fetch_add_explicit(slowFramesCounter, 1, memory_order_relaxed);}if (frameDuration > kFPRFrozenFrameThreshold) {atomic_fetch_add_explicit(frozenFramesCounter, 1, memory_order_relaxed);}atomic_fetch_add_explicit(totalFramesCounter, 1, memory_order_relaxed);}#pragma mark - Helper methods/** Starts a screen trace for the given UIViewController instance if it doesn't exist. This method* does NOT ensure thread safety - the caller is responsible for making sure that this is invoked* in a thread safe manner.** @param viewController The UIViewController instance for which the trace is to be started.* @param currentTotalFrames The value of the totalFramesCount before this method was called.* @param currentFrozenFrames The value of the frozenFramesCount before this method was called.* @param currentSlowFrames The value of the slowFramesCount before this method was called.*/- (void)startScreenTraceForViewController:(UIViewController *)viewControllercurrentTotalFrames:(int64_t)currentTotalFramescurrentFrozenFrames:(int64_t)currentFrozenFramescurrentSlowFrames:(int64_t)currentSlowFrames {if (![self shouldCreateScreenTraceForViewController:viewController]) {return;}// If there's a trace for this viewController, don't do anything.if (![self.activeScreenTraces objectForKey:viewController]) {NSString *traceName = FPRScreenTraceNameForViewController(viewController);FIRTrace *newTrace = [[FIRTrace alloc] initInternalTraceWithName:traceName];[newTrace start];[newTrace setIntValue:currentTotalFrames forMetric:kFPRTotalFramesCounterName];[newTrace setIntValue:currentFrozenFrames forMetric:kFPRFrozenFrameCounterName];[newTrace setIntValue:currentSlowFrames forMetric:kFPRSlowFrameCounterName];[self.activeScreenTraces setObject:newTrace forKey:viewController];}}/** Stops a screen trace for the given UIViewController instance if it exist. This method does NOT* ensure thread safety - the caller is responsible for making sure that this is invoked in a* thread safe manner.** @param viewController The UIViewController instance for which the trace is to be stopped.* @param currentTotalFrames The value of the totalFramesCount before this method was called.* @param currentFrozenFrames The value of the frozenFramesCount before this method was called.* @param currentSlowFrames The value of the slowFramesCount before this method was called.*/- (void)stopScreenTraceForViewController:(UIViewController *)viewControllercurrentTotalFrames:(int64_t)currentTotalFramescurrentFrozenFrames:(int64_t)currentFrozenFramescurrentSlowFrames:(int64_t)currentSlowFrames {FIRTrace *previousScreenTrace = [self.activeScreenTraces objectForKey:viewController];// Get a diff between the counters now and what they were at trace start.int64_t actualTotalFrames =currentTotalFrames - [previousScreenTrace valueForIntMetric:kFPRTotalFramesCounterName];int64_t actualFrozenFrames =currentFrozenFrames - [previousScreenTrace valueForIntMetric:kFPRFrozenFrameCounterName];int64_t actualSlowFrames =currentSlowFrames - [previousScreenTrace valueForIntMetric:kFPRSlowFrameCounterName];// Update the values in the trace.if (actualTotalFrames != 0) {[previousScreenTrace setIntValue:actualTotalFrames forMetric:kFPRTotalFramesCounterName];} else {[previousScreenTrace deleteMetric:kFPRTotalFramesCounterName];}if (actualFrozenFrames != 0) {[previousScreenTrace setIntValue:actualFrozenFrames forMetric:kFPRFrozenFrameCounterName];} else {[previousScreenTrace deleteMetric:kFPRFrozenFrameCounterName];}if (actualSlowFrames != 0) {[previousScreenTrace setIntValue:actualSlowFrames forMetric:kFPRSlowFrameCounterName];} else {[previousScreenTrace deleteMetric:kFPRSlowFrameCounterName];}if (previousScreenTrace.numberOfCounters > 0) {[previousScreenTrace stop];} else {// The trace did not collect any data. Don't log it.[previousScreenTrace cancel];}[self.activeScreenTraces removeObjectForKey:viewController];}#pragma mark - Filtering for screen traces/** Determines whether to create a screen trace for the given UIViewController instance.** @param viewController The UIViewController instance.* @return YES if a screen trace should be created for the given UIViewController instance,NO otherwise.*/- (BOOL)shouldCreateScreenTraceForViewController:(UIViewController *)viewController {if (viewController == nil) {return NO;}// Ignore non-main bundle view controllers whose class or superclass is an internal iOS view// controller. This is borrowed from the logic for tracking screens in Firebase Analytics.NSBundle *bundle = [NSBundle bundleForClass:[viewController class]];if (bundle != [NSBundle mainBundle]) {NSString *className = FPRUnprefixedClassName([viewController class]);if ([className hasPrefix:@"_"]) {return NO;}NSString *superClassName = FPRUnprefixedClassName([viewController superclass]);if ([superClassName hasPrefix:@"_"]) {return NO;}}// We are not creating screen traces for these view controllers because they're container view// controllers. They always have a child view controller which will provide better context for a// screen trace. We are however capturing traces if a developer subclasses these as there may be// some context. Special case: We are not capturing screen traces for any input view// controllers.return !([viewController isMemberOfClass:[UINavigationController class]] ||[viewController isMemberOfClass:[UITabBarController class]] ||[viewController isMemberOfClass:[UISplitViewController class]] ||[viewController isMemberOfClass:[UIPageViewController class]] ||[viewController isKindOfClass:[UIInputViewController class]]);}#pragma mark - Screen Traces swizzling hooks- (void)viewControllerDidAppear:(UIViewController *)viewController {// To get the most accurate numbers of total, frozen and slow frames, we need to capture them as// soon as we're notified of an event.int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);int64_t currentFrozenFrames = atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);int64_t currentSlowFrames = atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);dispatch_sync(self.screenTraceTrackerSerialQueue, ^{[self startScreenTraceForViewController:viewControllercurrentTotalFrames:currentTotalFramescurrentFrozenFrames:currentFrozenFramescurrentSlowFrames:currentSlowFrames];});}- (void)viewControllerDidDisappear:(id)viewController {// To get the most accurate numbers of total, frozen and slow frames, we need to capture them as// soon as we're notified of an event.int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);int64_t currentFrozenFrames = atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);int64_t currentSlowFrames = atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);dispatch_sync(self.screenTraceTrackerSerialQueue, ^{[self stopScreenTraceForViewController:viewControllercurrentTotalFrames:currentTotalFramescurrentFrozenFrames:currentFrozenFramescurrentSlowFrames:currentSlowFrames];});}#pragma mark - Test Helper Methods- (int_fast64_t)totalFramesCount {return atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);}- (void)setTotalFramesCount:(int_fast64_t)count {atomic_store_explicit(&_totalFramesCount, count, memory_order_relaxed);}- (int_fast64_t)slowFramesCount {return atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);}- (void)setSlowFramesCount:(int_fast64_t)count {atomic_store_explicit(&_slowFramesCount, count, memory_order_relaxed);}- (int_fast64_t)frozenFramesCount {return atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);}- (void)setFrozenFramesCount:(int_fast64_t)count {atomic_store_explicit(&_frozenFramesCount, count, memory_order_relaxed);}@end