1. What causes gridlock

  • A deadlock
  • Grab the lock
  • Lots of Ui drawing, complex Ui, text mix
  • The main thread has a lot of IO and a lot of computation

2. Supplementary knowledge – semaphore

A semaphore is a resource counter that has two operations, P and V, to achieve mutual exclusion. In general, critical access or mutually exclusive access is carried out as follows: set the signal magnitude value as 1, and when a process 1 runs, use the resource to perform P operation, that is, to reduce the signal magnitude value by 1, that is, the number of resources is 1 less, then the signal magnitude value is 0.

In the system, when the signal magnitude value is 0, the operation must wait until the signal magnitude value is not zero. In this case, if process 2 wants to run, it must also perform P operation, but at this time the semaphore is 0, so it cannot be reduced by 1, that is, it cannot perform P operation, which is blocked, and then it reaches process 1 exclusive access.

When process 1 is complete, release resources and perform operation V. When the number of resources is increased by 1, the semaphore value becomes 1. At this time, process 2 finds that the number of resources is not 0, and the semaphore can perform P operation, and immediately perform P operation. The semaphore value is 0 again, and process 2 has resources and exclusive access to resources. This is how semaphores control mutexes.

3. Look for caton entry points

RunLoop detection

Monitoring gridlock is about finding out what the main thread is doing. We know that a thread’s message event processing is driven by NSRunLoop, so to know what method the thread is calling, we need to start with NSRunLoop. The CFRunLoop code is open source. You can check out the cfrunloop. c source code here

The simplified logic of the core method CFRunLoopRun looks something like this:

Observers are about to create AutoreleasePool: _objc_autoreleasePoolPush(); /// 1. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry); Do {/// 2. Notify Observers that a Timer callback is about to occur. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers); Notifying Observers that Source (non-port-based,Source0) callback is about to occur. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// 4. Trigger the Source0 (non-port-based) callback. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0); // 5. GCD handles main block __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// Observers are in this state torelease and create AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting); /// 7. sleep to wait msg. mach_msg() -> mach_msg_trap(); Observers, __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting); /// 8. /// 9. If the Timer wakes up, call Timer __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(Timer); /// 9. Block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block); If Runloop is woken up by Source1 (port based) event, Handle this event __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1); } while (...) ; Observers are about torelease AutoreleasePool: _objc_autoreleasePoolPop(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit); }Copy the code

It is not difficult to find that the NSRunLoop call method is mainly between kCFRunLoopBeforeSources and kCFRunLoopBeforeWaiting, and after kCFRunLoopAfterWaiting, that is, if we find that the time between the two is too long, then You can determine that the main thread is stuck at this time.

How does iOS monitor thread stalling?

Talk about the general implementation ideas in QiLagMonitor.

  • First, create a runLoopObserver to observe the runloop status of the main thread. A dispatchSemaphore is also created to ensure synchronous operations.

  • Second, add the runLoopObserver to the main thread runloop to observe.

  • Then, a child thread is opened and a continuous loop is opened in the child thread to monitor the status of the main thread runloop.

  • If the status of the main thread runloop is stuck in BeforeSources or AfterWaiting for more than 88 milliseconds, the main thread is currently stuck. In this case, we save the main thread’s current call stack for monitoring purposes.

4. Quantify the degree of Caton

To monitor the state of NSRunLoop, we need to use CFRunLoopObserverRef, through which the changes of these state values can be obtained in real time. The specific uses are as follows:

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MyClass *object = (__bridge MyClass*)info;
    object->activity = activity;
}

- (void)registerObserver
{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            0,
                                                            &runLoopObserverCallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
Copy the code

The UI focuses on __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0); And __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ (source1); Before. Get the status of kCFRunLoopBeforeSources to kCFRunLoopBeforeWaiting and then to kCFRunLoopAfterWaiting to know if there is a lag.

How to do it: Just start another thread and calculate in real time whether the time between the two states has reached a certain threshold to catch these performance killers.

  • Listen for runloop status changes

    Static void runLoopObserverCallBack(CFRunLoopObserverRef Observer, CFRunLoopActivity activity, void *info) { BGPerformanceMonitor monitor = (__bridge BGPerformanceMonitor)info;

    Monitor ->activity = activity; // dispatch_semaphore_t semaphore = monitor->semaphore; long st = dispatch_semaphore_signal(semaphore); NSLog(@"dispatch_semaphore_signal:st=%ld,time:%@",st,[BGPerformanceMonitor getCurTime]); /* Run Loop Observer Activities */Copy the code

    // typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { // kCFRunLoopEntry = (1UL << 0), // kCFRunLoopBeforeTimers = (1UL << 1), // kCFRunLoopBeforeSources = (1UL << 2), // kCFRunLoopAfterWaiting = (1UL << 6), // kCFRunLoopAfterWaiting = (1UL << 6), // kCFRunLoopExit = (1UL << 7), // kCFRunLoopAllActivities = 0x0FFFFFFFU //};

    If (activity == kCFRunLoopEntry) {// About to enter RunLoop NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopEntry"); } else if (activity == kCFRunLoopBeforeTimers) {// About to process Timer NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeTimers"); } else if (activity == kCFRunLoopBeforeSources) {// Source NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeSources"); } else if (activity == kCFRunLoopBeforeWaiting) {NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopBeforeWaiting"); } else if (activity == kCFRunLoopAfterWaiting) {NSLog(@"runLoopObserverCallBack -) just woke up from sleep %@",@"kCFRunLoopAfterWaiting"); } else if (activity == kCFRunLoopExit) {RunLoop NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopExit"); } else if (activity == kCFRunLoopAllActivities) { NSLog(@"runLoopObserverCallBack - %@",@"kCFRunLoopAllActivities"); }Copy the code

    }

  • Enable runloop listening

    // Start listening

    • (void)startMonitor { if (observer) { return; }

      Semaphore = dispatch_semaphoRE_create (0); NSLog(@”dispatch_semaphore_create:%@”,[BGPerformanceMonitor getCurTime]);

      CFRunLoopObserverContext Context = {0,(__bridge void*)self,NULL,NULL}; // The first argument is used to allocate the memory of the observer. // The second argument is used to set the events that the observer is concerned with. // The third parameter specifies whether the Observer is executed on the first or every run loop entry // The fourth parameter sets the priority of the Observer Observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

      Dispatch_async (dispatch_get_global_queue(0, 0)) ^{while (YES) {// Query the status of the current runloop if there is a signal The state change callback method runLoopObserverCallBack increments the semaphore by 1, so each time the state of the runloop changes, the following statement will execute // dispatch_semaphore_wait:Returns zero on success, or non-zero if the timeout occurred. long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC)); NSLog(@”dispatch_semaphore_wait:st=%ld,time:%@”,st,[self getCurTime]); if (st ! If (! = 0) {// The semaphore has timed out. observer) { timeoutCount = 0; semaphore = 0; activity = 0; return; } NSLog(@”st = %ld,activity = %lu,timeoutCount = %d,time:%@”,st,activity,timeoutCount,[self getCurTime]); // kCFRunLoopBeforeSources – about to process source kCFRunLoopAfterWaiting – just woke up from sleep // Get the status of kCFRunLoopBeforeSources to kCFRunLoopBeforeWaiting and then to kCFRunLoopAfterWaiting to know if there is a lag. / / kCFRunLoopBeforeSources: stay in this state, says do lots of things in the if (activity = = kCFRunLoopBeforeSources | | activity = = KCFRunLoopAfterWaiting) {if (++timeoutCount < 5) {continue; // Continue the next loop without setting timeoutCount to 0}

      PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]; PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config]; NSData *data = [crashReporter generateLiveReport]; PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL]; NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter withTextFormat:PLCrashReportTextFormatiOS]; NSLog (@ "-- -- -- -- -- -- -- -- -- caton information \ n % @ \ n -- -- -- -- -- -- -- -- -- -- -- -- -- --", the report). }} NSLog(@"dispatch_semaphore_wait timeoutCount = 0, time:%@",[self getCurTime]); timeoutCount = 0; }Copy the code

      });

    }

Log caton’s function calls

Of course, the next step is to record the function call information at this time. PLCrashReporter, a third-party Crash collection component, can not only collect Crash information but also obtain the call stack of each thread in real time, as shown in the following example:

PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
                                                                   symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
NSData *data = [crashReporter generateLiveReport];
PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
                                                          withTextFormat:PLCrashReportTextFormatiOS];
NSLog(@"------------\n%@\n------------", report);
Copy the code

When detected, grab stack information, and then do some filtering on the client, it can be reported to the server, through the collection of a certain amount of stuck data after analysis will be able to accurately locate the need to optimize the logic, so far this real-time stuck monitoring is done!

The child thread Ping

However, since the main thread RunLoop is basically in a Before Waiting state when idle, this detection method can always identify the main thread as being stalled even if nothing has occurred. The main thread must be between kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting. Set the flag bit to YES for each detection, and then send the task to the main thread and set the flag bit to NO. Then the child thread sleeps the timeout threshold and determines whether the flag bit is successfully set to NO. If it does not indicate that the main thread has stalled. In ANREye, the child thread Ping is used to detect the lag.

@interface PingThread : NSThread ...... @end @implementation PingThread - (void)main { [self pingMainThread]; } - (void)pingMainThread { while (! self.cancelled) { @autoreleasepool { dispatch_async(dispatch_get_main_queue(), ^{ [_lock unlock]; }); CFAbsoluteTime pingTime = CFAbsoluteTimeGetCurrent(); NSArray *callSymbols = [StackBacktrace backtraceMainThread]; [_lock lock]; if (CFAbsoluteTimeGetCurrent() - pingTime >= _threshold) { ...... } [NSThread sleepForTimeInterval: _interval]; } } } @endCopy the code

Catton method stack information

When we get the time of the block, we need to get the stack of the block immediately. There are two ways to get the stack frame. One is to walk through the stack frame, and I get the stack of any thread on iOS, which is very detailed, and I open-source the code RCBacktrace. Backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift

The test case

Write a tableView, drag it up and down, and set it artificially to stall.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 100; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (! cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } NSString *text = nil; If (indexPath. Row % 10 == 0) {// Sleep 0.2s usleep(500 * 1000); // 1 * 1000 * 1000 == 1 SEC text = @" I'm doing something complicated, I need some time "; } else { text = [NSString stringWithFormat:@"cell - %ld",indexPath.row]; } cell.textLabel.text = text; return cell; } Running result 2.1: We can find that the time interval of each printing exceeds 50ms, that is to say, the timeout time of the semaphore has reached, then it will enter if(st! Activity = 4, the current state of runloop is kCFRunLoopBeforeSources, which indicates that many things are being done. TimeoutCount Specifies the number of timeouts to be accumulated. Running Result 2.2: Because the number of timeoutCount times has reached 5 times and the current runloop state is kCFRunLoopBeforeSources or kCFRunLoopAfterWaiting, we can determine that a delay has occurred and obtain the current stack information. PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]; PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config]; NSData *data = [crashReporter generateLiveReport]; PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL]; NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter withTextFormat:PLCrashReportTextFormatiOS]; NSLog (@ "-- -- -- -- -- -- -- -- -- caton information \ n % @ \ n -- -- -- -- -- -- -- -- -- -- -- -- -- --", the report).Copy the code

The second FPS monitoring

Typically, screens refresh at 60hz/s, with a screen refresh signal emitted per refresh, and CADisplayLink allows us to register a callback processing synchronized with the refresh signal. FPS values can be displayed via the screen refresh mechanism:

Methods a

@implementation ViewController { UILabel *_fpsLbe; CADisplayLink *_link; NSTimeInterval _lastTime; float _fps; } - (void)startMonitoring { if (_link) { [_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];  [_link invalidate]; _link = nil; } _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)]; [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } - (void)fpsDisplayLinkAction:(CADisplayLink *)link { if (_lastTime == 0) { _lastTime = link.timestamp; return; } self.count++; NSTimeInterval delta = link.timestamp - _lastTime; if (delta < 1) return; _lastTime = link.timestamp; _fps = _count / delta; NSLog(@"count = %d, delta = %f,_lastTime = %f, _fps = %.0f",_count, delta, _lastTime, _fps); self.count = 0; _fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps]; }Copy the code

Listen for changes in count

#pragma mark - observer - (void)addObserver { [self addObserver:self forKeyPath:@"count" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"count new = %@, old = %@",[change valueForKey:@"new"], [change valueForKey:@"old"]); } 1. By printing, we know that each refresh will emit a screen refresh signal, and the callback method 'fpsDisplayLinkAction:' synchronized with the refresh signal will be called, then count plus one. 2. Every second, we calculate the 'FPS' value, using a variable _lastTime to record the last time to calculate the FPS value, and then divide count by the time interval to get the FPS value. After reassigning _lastTime, _count is set to zero. 3. Normally, the screen refreshes at 60 Hz /s, so the 'fpsDisplayLinkAction:' method is called 60 times in 1 second. The value of FPS calculation is 0, it does not lag, smooth. 4. If 'fpsDisplayLinkAction:' only calls back 50 times in 1 second, the calculated FPS is _count/delta.Copy the code
Method 2
- (void)startFpsMonitoring { _link = [CADisplayLink displayLinkWithTarget: self selector: @selector(displayFps:)]; [_link addToRunLoop: [NSRunLoop mainRunLoop] forMode: NSRunLoopCommonModes]; } - (void)displayFps: (CADisplayLink *)fpsDisplay { self.count++; CFAbsoluteTime threshold = CFAbsoluteTimeGetCurrent() - _lastTime; If (threshold >= 1.0) {_fps = (_count/threshold); _lastTime = CFAbsoluteTimeGetCurrent(); _fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps]; self.count = 0; NSLog(@"count = %d,_lastTime = %f, _fps = %.0f",_count, _lastTime, _fps); }}Copy the code

The result is similar to method one.