FPRScreenTraceTracker.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. // Copyright 2020 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import "FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker.h"
  15. #import "FirebasePerformance/Sources/AppActivity/FPRScreenTraceTracker+Private.h"
  16. #import <Foundation/Foundation.h>
  17. #import <UIKit/UIKit.h>
  18. #import "FirebasePerformance/Sources/Common/FPRDiagnostics.h"
  19. NSString *const kFPRPrefixForScreenTraceName = @"_st_";
  20. NSString *const kFPRFrozenFrameCounterName = @"_fr_fzn";
  21. NSString *const kFPRSlowFrameCounterName = @"_fr_slo";
  22. NSString *const kFPRTotalFramesCounterName = @"_fr_tot";
  23. // Note: This was previously 60 FPS, but that resulted in 90% + of all frames collected to be
  24. // flagged as slow frames, and so the threshold for iOS is being changed to 59 FPS.
  25. // TODO(b/73498642): Make these configurable.
  26. CFTimeInterval const kFPRSlowFrameThreshold = 1.0 / 59.0; // Anything less than 59 FPS is slow.
  27. CFTimeInterval const kFPRFrozenFrameThreshold = 700.0 / 1000.0;
  28. /** Constant that indicates an invalid time. */
  29. CFAbsoluteTime const kFPRInvalidTime = -1.0;
  30. /** Returns the class name without the prefixed module name present in Swift classes
  31. * (e.g. MyModule.MyViewController -> MyViewController).
  32. */
  33. static NSString *FPRUnprefixedClassName(Class theClass) {
  34. NSString *className = NSStringFromClass(theClass);
  35. NSRange periodRange = [className rangeOfString:@"." options:NSBackwardsSearch];
  36. if (periodRange.location == NSNotFound) {
  37. return className;
  38. }
  39. return periodRange.location < className.length - 1
  40. ? [className substringFromIndex:periodRange.location + 1]
  41. : className;
  42. }
  43. /** Returns the name for the screen trace for a given UIViewController. It does the following:
  44. * - Removes module name from swift classes - (e.g. MyModule.MyViewController -> MyViewController)
  45. * - Prepends "_st_" to the class name
  46. * - Truncates the length if it exceeds the maximum trace length.
  47. *
  48. * @param viewController The view controller whose screen trace name we want. Cannot be nil.
  49. * @return An NSString containing the trace name, or a string containing an error if the
  50. * class was nil.
  51. */
  52. static NSString *FPRScreenTraceNameForViewController(UIViewController *viewController) {
  53. NSString *unprefixedClassName = FPRUnprefixedClassName([viewController class]);
  54. if (unprefixedClassName.length != 0) {
  55. NSString *traceName =
  56. [NSString stringWithFormat:@"%@%@", kFPRPrefixForScreenTraceName, unprefixedClassName];
  57. return traceName.length > kFPRMaxNameLength ? [traceName substringToIndex:kFPRMaxNameLength]
  58. : traceName;
  59. } else {
  60. // This is unlikely, but might happen if there's a regression on iOS where the class name
  61. // returned for a non-nil class is nil or empty.
  62. return @"_st_ERROR_NIL_CLASS_NAME";
  63. }
  64. }
  65. @implementation FPRScreenTraceTracker {
  66. /** Instance variable storing the total frames observed so far. */
  67. atomic_int_fast64_t _totalFramesCount;
  68. /** Instance variable storing the slow frames observed so far. */
  69. atomic_int_fast64_t _slowFramesCount;
  70. /** Instance variable storing the frozen frames observed so far. */
  71. atomic_int_fast64_t _frozenFramesCount;
  72. }
  73. @dynamic totalFramesCount;
  74. @dynamic frozenFramesCount;
  75. @dynamic slowFramesCount;
  76. + (instancetype)sharedInstance {
  77. static FPRScreenTraceTracker *instance;
  78. static dispatch_once_t onceToken;
  79. dispatch_once(&onceToken, ^{
  80. instance = [[self alloc] init];
  81. });
  82. return instance;
  83. }
  84. - (instancetype)init {
  85. self = [super init];
  86. if (self) {
  87. // Weakly retain viewController, use pointer hashing.
  88. NSMapTableOptions keyOptions = NSMapTableWeakMemory | NSMapTableObjectPointerPersonality;
  89. // Strongly retain the FIRTrace.
  90. NSMapTableOptions valueOptions = NSMapTableStrongMemory;
  91. _activeScreenTraces = [NSMapTable mapTableWithKeyOptions:keyOptions valueOptions:valueOptions];
  92. _previouslyVisibleViewControllers = nil; // Will be set when there is data.
  93. _screenTraceTrackerSerialQueue =
  94. dispatch_queue_create("com.google.FPRScreenTraceTracker", DISPATCH_QUEUE_SERIAL);
  95. _screenTraceTrackerDispatchGroup = dispatch_group_create();
  96. atomic_store_explicit(&_totalFramesCount, 0, memory_order_relaxed);
  97. atomic_store_explicit(&_frozenFramesCount, 0, memory_order_relaxed);
  98. atomic_store_explicit(&_slowFramesCount, 0, memory_order_relaxed);
  99. _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkStep)];
  100. [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
  101. // We don't receive background and foreground events from analytics and so we have to listen to
  102. // them ourselves.
  103. [[NSNotificationCenter defaultCenter] addObserver:self
  104. selector:@selector(appDidBecomeActiveNotification:)
  105. name:UIApplicationDidBecomeActiveNotification
  106. object:[UIApplication sharedApplication]];
  107. [[NSNotificationCenter defaultCenter] addObserver:self
  108. selector:@selector(appWillResignActiveNotification:)
  109. name:UIApplicationWillResignActiveNotification
  110. object:[UIApplication sharedApplication]];
  111. }
  112. return self;
  113. }
  114. - (void)dealloc {
  115. [_displayLink invalidate];
  116. [[NSNotificationCenter defaultCenter] removeObserver:self
  117. name:UIApplicationDidBecomeActiveNotification
  118. object:[UIApplication sharedApplication]];
  119. [[NSNotificationCenter defaultCenter] removeObserver:self
  120. name:UIApplicationWillResignActiveNotification
  121. object:[UIApplication sharedApplication]];
  122. }
  123. - (void)appDidBecomeActiveNotification:(NSNotification *)notification {
  124. // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as
  125. // soon as we're notified of an event.
  126. int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);
  127. int64_t currentFrozenFrames = atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);
  128. int64_t currentSlowFrames = atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);
  129. dispatch_group_async(self.screenTraceTrackerDispatchGroup, self.screenTraceTrackerSerialQueue, ^{
  130. for (id viewController in self.previouslyVisibleViewControllers) {
  131. [self startScreenTraceForViewController:viewController
  132. currentTotalFrames:currentTotalFrames
  133. currentFrozenFrames:currentFrozenFrames
  134. currentSlowFrames:currentSlowFrames];
  135. }
  136. self.previouslyVisibleViewControllers = nil;
  137. });
  138. }
  139. - (void)appWillResignActiveNotification:(NSNotification *)notification {
  140. // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as
  141. // soon as we're notified of an event.
  142. int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);
  143. int64_t currentFrozenFrames = atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);
  144. int64_t currentSlowFrames = atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);
  145. dispatch_group_async(self.screenTraceTrackerDispatchGroup, self.screenTraceTrackerSerialQueue, ^{
  146. self.previouslyVisibleViewControllers = [NSPointerArray weakObjectsPointerArray];
  147. id visibleViewControllersEnumerator = [self.activeScreenTraces keyEnumerator];
  148. id visibleViewController = nil;
  149. while (visibleViewController = [visibleViewControllersEnumerator nextObject]) {
  150. [self.previouslyVisibleViewControllers addPointer:(__bridge void *)(visibleViewController)];
  151. }
  152. for (id visibleViewController in self.previouslyVisibleViewControllers) {
  153. [self stopScreenTraceForViewController:visibleViewController
  154. currentTotalFrames:currentTotalFrames
  155. currentFrozenFrames:currentFrozenFrames
  156. currentSlowFrames:currentSlowFrames];
  157. }
  158. });
  159. }
  160. #pragma mark - Frozen, slow and good frames
  161. - (void)displayLinkStep {
  162. static CFAbsoluteTime previousTimestamp = kFPRInvalidTime;
  163. CFAbsoluteTime currentTimestamp = self.displayLink.timestamp;
  164. RecordFrameType(currentTimestamp, previousTimestamp, &_slowFramesCount, &_frozenFramesCount,
  165. &_totalFramesCount);
  166. previousTimestamp = currentTimestamp;
  167. }
  168. /** This function increments the relevant frame counters based on the current and previous
  169. * timestamp provided by the displayLink.
  170. *
  171. * @param currentTimestamp The current timestamp of the displayLink.
  172. * @param previousTimestamp The previous timestamp of the displayLink.
  173. * @param slowFramesCounter The value of the slowFramesCount before this function was called.
  174. * @param frozenFramesCounter The value of the frozenFramesCount before this function was called.
  175. * @param totalFramesCounter The value of the totalFramesCount before this function was called.
  176. */
  177. FOUNDATION_STATIC_INLINE
  178. void RecordFrameType(CFAbsoluteTime currentTimestamp,
  179. CFAbsoluteTime previousTimestamp,
  180. atomic_int_fast64_t *slowFramesCounter,
  181. atomic_int_fast64_t *frozenFramesCounter,
  182. atomic_int_fast64_t *totalFramesCounter) {
  183. CFTimeInterval frameDuration = currentTimestamp - previousTimestamp;
  184. if (previousTimestamp == kFPRInvalidTime) {
  185. return;
  186. }
  187. if (frameDuration > kFPRSlowFrameThreshold) {
  188. atomic_fetch_add_explicit(slowFramesCounter, 1, memory_order_relaxed);
  189. }
  190. if (frameDuration > kFPRFrozenFrameThreshold) {
  191. atomic_fetch_add_explicit(frozenFramesCounter, 1, memory_order_relaxed);
  192. }
  193. atomic_fetch_add_explicit(totalFramesCounter, 1, memory_order_relaxed);
  194. }
  195. #pragma mark - Helper methods
  196. /** Starts a screen trace for the given UIViewController instance if it doesn't exist. This method
  197. * does NOT ensure thread safety - the caller is responsible for making sure that this is invoked
  198. * in a thread safe manner.
  199. *
  200. * @param viewController The UIViewController instance for which the trace is to be started.
  201. * @param currentTotalFrames The value of the totalFramesCount before this method was called.
  202. * @param currentFrozenFrames The value of the frozenFramesCount before this method was called.
  203. * @param currentSlowFrames The value of the slowFramesCount before this method was called.
  204. */
  205. - (void)startScreenTraceForViewController:(UIViewController *)viewController
  206. currentTotalFrames:(int64_t)currentTotalFrames
  207. currentFrozenFrames:(int64_t)currentFrozenFrames
  208. currentSlowFrames:(int64_t)currentSlowFrames {
  209. if (![self shouldCreateScreenTraceForViewController:viewController]) {
  210. return;
  211. }
  212. // If there's a trace for this viewController, don't do anything.
  213. if (![self.activeScreenTraces objectForKey:viewController]) {
  214. NSString *traceName = FPRScreenTraceNameForViewController(viewController);
  215. FIRTrace *newTrace = [[FIRTrace alloc] initInternalTraceWithName:traceName];
  216. [newTrace start];
  217. [newTrace setIntValue:currentTotalFrames forMetric:kFPRTotalFramesCounterName];
  218. [newTrace setIntValue:currentFrozenFrames forMetric:kFPRFrozenFrameCounterName];
  219. [newTrace setIntValue:currentSlowFrames forMetric:kFPRSlowFrameCounterName];
  220. [self.activeScreenTraces setObject:newTrace forKey:viewController];
  221. }
  222. }
  223. /** Stops a screen trace for the given UIViewController instance if it exist. This method does NOT
  224. * ensure thread safety - the caller is responsible for making sure that this is invoked in a
  225. * thread safe manner.
  226. *
  227. * @param viewController The UIViewController instance for which the trace is to be stopped.
  228. * @param currentTotalFrames The value of the totalFramesCount before this method was called.
  229. * @param currentFrozenFrames The value of the frozenFramesCount before this method was called.
  230. * @param currentSlowFrames The value of the slowFramesCount before this method was called.
  231. */
  232. - (void)stopScreenTraceForViewController:(UIViewController *)viewController
  233. currentTotalFrames:(int64_t)currentTotalFrames
  234. currentFrozenFrames:(int64_t)currentFrozenFrames
  235. currentSlowFrames:(int64_t)currentSlowFrames {
  236. FIRTrace *previousScreenTrace = [self.activeScreenTraces objectForKey:viewController];
  237. // Get a diff between the counters now and what they were at trace start.
  238. int64_t actualTotalFrames =
  239. currentTotalFrames - [previousScreenTrace valueForIntMetric:kFPRTotalFramesCounterName];
  240. int64_t actualFrozenFrames =
  241. currentFrozenFrames - [previousScreenTrace valueForIntMetric:kFPRFrozenFrameCounterName];
  242. int64_t actualSlowFrames =
  243. currentSlowFrames - [previousScreenTrace valueForIntMetric:kFPRSlowFrameCounterName];
  244. // Update the values in the trace.
  245. if (actualTotalFrames != 0) {
  246. [previousScreenTrace setIntValue:actualTotalFrames forMetric:kFPRTotalFramesCounterName];
  247. } else {
  248. [previousScreenTrace deleteMetric:kFPRTotalFramesCounterName];
  249. }
  250. if (actualFrozenFrames != 0) {
  251. [previousScreenTrace setIntValue:actualFrozenFrames forMetric:kFPRFrozenFrameCounterName];
  252. } else {
  253. [previousScreenTrace deleteMetric:kFPRFrozenFrameCounterName];
  254. }
  255. if (actualSlowFrames != 0) {
  256. [previousScreenTrace setIntValue:actualSlowFrames forMetric:kFPRSlowFrameCounterName];
  257. } else {
  258. [previousScreenTrace deleteMetric:kFPRSlowFrameCounterName];
  259. }
  260. if (previousScreenTrace.numberOfCounters > 0) {
  261. [previousScreenTrace stop];
  262. } else {
  263. // The trace did not collect any data. Don't log it.
  264. [previousScreenTrace cancel];
  265. }
  266. [self.activeScreenTraces removeObjectForKey:viewController];
  267. }
  268. #pragma mark - Filtering for screen traces
  269. /** Determines whether to create a screen trace for the given UIViewController instance.
  270. *
  271. * @param viewController The UIViewController instance.
  272. * @return YES if a screen trace should be created for the given UIViewController instance,
  273. NO otherwise.
  274. */
  275. - (BOOL)shouldCreateScreenTraceForViewController:(UIViewController *)viewController {
  276. if (viewController == nil) {
  277. return NO;
  278. }
  279. // Ignore non-main bundle view controllers whose class or superclass is an internal iOS view
  280. // controller. This is borrowed from the logic for tracking screens in Firebase Analytics.
  281. NSBundle *bundle = [NSBundle bundleForClass:[viewController class]];
  282. if (bundle != [NSBundle mainBundle]) {
  283. NSString *className = FPRUnprefixedClassName([viewController class]);
  284. if ([className hasPrefix:@"_"]) {
  285. return NO;
  286. }
  287. NSString *superClassName = FPRUnprefixedClassName([viewController superclass]);
  288. if ([superClassName hasPrefix:@"_"]) {
  289. return NO;
  290. }
  291. }
  292. // We are not creating screen traces for these view controllers because they're container view
  293. // controllers. They always have a child view controller which will provide better context for a
  294. // screen trace. We are however capturing traces if a developer subclasses these as there may be
  295. // some context. Special case: We are not capturing screen traces for any input view
  296. // controllers.
  297. return !([viewController isMemberOfClass:[UINavigationController class]] ||
  298. [viewController isMemberOfClass:[UITabBarController class]] ||
  299. [viewController isMemberOfClass:[UISplitViewController class]] ||
  300. [viewController isMemberOfClass:[UIPageViewController class]] ||
  301. [viewController isKindOfClass:[UIInputViewController class]]);
  302. }
  303. #pragma mark - Screen Traces swizzling hooks
  304. - (void)viewControllerDidAppear:(UIViewController *)viewController {
  305. // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as
  306. // soon as we're notified of an event.
  307. int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);
  308. int64_t currentFrozenFrames = atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);
  309. int64_t currentSlowFrames = atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);
  310. dispatch_sync(self.screenTraceTrackerSerialQueue, ^{
  311. [self startScreenTraceForViewController:viewController
  312. currentTotalFrames:currentTotalFrames
  313. currentFrozenFrames:currentFrozenFrames
  314. currentSlowFrames:currentSlowFrames];
  315. });
  316. }
  317. - (void)viewControllerDidDisappear:(id)viewController {
  318. // To get the most accurate numbers of total, frozen and slow frames, we need to capture them as
  319. // soon as we're notified of an event.
  320. int64_t currentTotalFrames = atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);
  321. int64_t currentFrozenFrames = atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);
  322. int64_t currentSlowFrames = atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);
  323. dispatch_sync(self.screenTraceTrackerSerialQueue, ^{
  324. [self stopScreenTraceForViewController:viewController
  325. currentTotalFrames:currentTotalFrames
  326. currentFrozenFrames:currentFrozenFrames
  327. currentSlowFrames:currentSlowFrames];
  328. });
  329. }
  330. #pragma mark - Test Helper Methods
  331. - (int_fast64_t)totalFramesCount {
  332. return atomic_load_explicit(&_totalFramesCount, memory_order_relaxed);
  333. }
  334. - (void)setTotalFramesCount:(int_fast64_t)count {
  335. atomic_store_explicit(&_totalFramesCount, count, memory_order_relaxed);
  336. }
  337. - (int_fast64_t)slowFramesCount {
  338. return atomic_load_explicit(&_slowFramesCount, memory_order_relaxed);
  339. }
  340. - (void)setSlowFramesCount:(int_fast64_t)count {
  341. atomic_store_explicit(&_slowFramesCount, count, memory_order_relaxed);
  342. }
  343. - (int_fast64_t)frozenFramesCount {
  344. return atomic_load_explicit(&_frozenFramesCount, memory_order_relaxed);
  345. }
  346. - (void)setFrozenFramesCount:(int_fast64_t)count {
  347. atomic_store_explicit(&_frozenFramesCount, count, memory_order_relaxed);
  348. }
  349. @end