Summary of iOS Caton monitoring scheme

After sorting out the schemes of Caton monitoring in iOS, if you don't understand the principle of Caton, you can read this article iOS's skill of keeping the interface smooth , well written.

FPS

FPS (Frames Per Second) is a definition in the image field, which represents the number of rendered frames per second. It is usually used to measure the smoothness of the picture. The more frames per second, the smoother the picture is. 60fps is the best. Generally, as long as the FPS of our APP is kept between 50-60, the user experience is relatively smooth.

There are several ways to monitor FPS. The most common way is to YYFPSLabel As seen in. The implementation principle is to add a CADisplayLink of commonModes to the RunLoop of the main thread. Every time the screen is refreshed, the CADisplayLink method must be executed. Therefore, you can count the number of screen refreshes within 1s, that is, FPS. Here is the code I implemented with Swift:

class WeakProxy: NSObject {
    
    weak var target: NSObjectProtocol?
    
    init(target: NSObjectProtocol) {
        self.target = target
        super.init()
    }
    
    override func responds(to aSelector: Selector!) -> Bool {
        return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
    }

    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        return target
    }
}

class FPSLabel: UILabel {
    var link:CADisplayLink!
    //Record method execution times
    var count: Int = 0
    //Record the time of the last method execution, and calculate the time interval through link.timestamp - ﹣ lasttime
    var lastTime: TimeInterval = 0
    var _font: UIFont!
    var _subFont: UIFont!
    
    fileprivate let defaultSize = CGSize(width: 55,height: 20)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        if frame.size.width == 0 && frame.size.height == 0 {
            self.frame.size = defaultSize
        }
        self.layer.cornerRadius = 5
        self.clipsToBounds = true
        self.textAlignment = NSTextAlignment.center
        self.isUserInteractionEnabled = false
        self.backgroundColor = UIColor.white.withAlphaComponent(0.7)
        
        _font = UIFont(name: "Menlo", size: 14)
        if _font != nil {
            _subFont = UIFont(name: "Menlo", size: 4)
        }else{
            _font = UIFont(name: "Courier", size: 14)
            _subFont = UIFont(name: "Courier", size: 4)
        }
        
        link = CADisplayLink(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.tick(link:)))
        link.add(to: RunLoop.main, forMode: .commonModes)
    }
    
    //How to refresh the CADisplayLink
    @objc func tick(link: CADisplayLink) {
        
        guard lastTime != 0 else {
            lastTime = link.timestamp
            return
        }
        
        count += 1
        let timePassed = link.timestamp - lastTime
        
        //It is calculated once every 1 second or more, that is, the refresh interval of FPSLabel. You do not want to refresh too often
        guard timePassed >= 1 else {
            return
        }
        lastTime = link.timestamp
        let fps = Double(count) / timePassed
        count = 0
        
        let progress = fps / 60.0
        let color = UIColor(hue: CGFloat(0.27 * (progress - 0.2)), saturation: 1, brightness: 0.9, alpha: 1)
        
        let text = NSMutableAttributedString(string: "\(Int(round(fps))) FPS")
        text.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: NSRange(location: 0, length: text.length - 3))
        text.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.white, range: NSRange(location: text.length - 3, length: 3))
        text.addAttribute(NSAttributedStringKey.font, value: _font, range: NSRange(location: 0, length: text.length))
        text.addAttribute(NSAttributedStringKey.font, value: _subFont, range: NSRange(location: text.length - 4, length: 1))
        self.attributedText = text
    }
    
    // Remove displaylin from Runloop modes
    deinit {
        link.invalidate()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

RunLoop

In fact, the use of CADisplayLink in FPS is also based on RunLoop, which all depends on main RunLoop. Let's take a look at the code of the short version of RunLoop

// 1. enter loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled)

// 2.RunLoop is about to trigger Timer callback.
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 3.RunLoop is about to trigger the Source0 (non port) callback.
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 4.RunLoop triggers the Source0 (non port) callback.
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle)
// 5. Execute the added block
__CFRunLoopDoBlocks(runloop, currentMode);

// 6. The thread of runloop is about to enter sleep.
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);

// 7. Call the mach? Msg to wait for the message of the mach? Port to be accepted. The thread will go to sleep until it is awakened by one of the following events.
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)

// Dormancy

// 8. The thread of runloop has just been awakened.
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting

// 9. If a Timer reaches the time, trigger the Timer's callback
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())

// 10. If there is a block from dispatch to main queue, execute bloc
 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
 
 // 11. If a Source1 (port based) sends an event, handle the event
__CFRunLoopDoSource1(runloop, currentMode, source1, msg);

// 12.RunLoop is about to exit
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

We can see that the RunLoop call methods are mainly between kcfrnloop beforesources and kcfrnloop afterwaiting. Some people may ask that there are also some method calls after kcfrnloop afterwaiting. Why not monitor? My understanding is that most of the methods that cause Caton are between kcfrnloop beforesources and kcfrnloop afterwaiting. For example, source0 mainly deals with App For internal events, the App is responsible for its own management (departure), such as UIEvent(Touch event, etc.), GS initiated to RunLoop to run and then event callback to UI), CFSocketRef. Open a sub thread, and then calculate whether the time between the two state areas of kcfrnloop before sources and kcfrnloop after waiting exceeds a certain threshold value in real time to determine the stuck situation of the main thread.

It's a little different here, iOS real-time Caton monitoring It is set to timeout for 5 times in a row for 50ms, and it is considered that Daying GCDFetchFeed The code set in is the code that is considered to be stuck when the timeout is 80ms for three times in a row. The following is the code provided in iOS real-time Caton monitoring:

- (void)start
{
    if (observer)
        return;
    
    // signal
    semaphore = dispatch_semaphore_create(0);
    
    // Register RunLoop status observation
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                       kCFRunLoopAllActivities,
                                       YES,
                                       0,
                                       &runLoopObserverCallBack,
                                       &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    // Monitoring time in child thread
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (!observer)
                {
                    timeoutCount = 0;
                    semaphore = 0;
                    activity = 0;
                    return;
                }
                
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount < 5)
                        continue;
                    
                    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);
                }
            }
            timeoutCount = 0;
        }
    });
}

Child thread Ping

But because the RunLoop of the main thread is basically in the Before Waiting state when it is idle, this detection method can always determine that the main thread is in the stuck state even if NO stuck occurs. The idea of this Caton monitoring scheme is to create a sub thread to ping the main thread by semaphore, because the main thread must be between kcfrnloopbeforesources and kcfrnloopfafterwaiting when ping. Set the flag bit to YES during each detection, and then send the task to the main thread to set the flag bit to NO. Next, determine whether the flag bit is set to NO successfully if the minimum sleep timeout time of the sub thread is long. If not, the main thread is stuck. ANREye It uses the method of sub thread Ping to monitor the Caton.

public class CatonMonitor {
    
    enum Constants {
        static let timeOutInterval: TimeInterval = 0.05
        static let queueTitle = "com.roy.PerformanceMonitor.CatonMonitor"
    }
    
    private var queue: DispatchQueue = DispatchQueue(label: Constants.queueTitle)
    private var isMonitoring = false
    private var semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
    
    public init() {}
    
    public func start() {
        guard !isMonitoring else { return }
        
        isMonitoring = true
        queue.async {
            while self.isMonitoring {
                
                var timeout = true
                
                DispatchQueue.main.async {
                    timeout = false
                    self.semaphore.signal()
                }
                
                Thread.sleep(forTimeInterval: Constants.timeOutInterval)
                
                if timeout {
                    let symbols = RCBacktrace.callstack(.main)
                    for symbol in symbols {
                        print(symbol.description)
                    }
                }
                self.semaphore.wait()
            }
        }
    }
    
    public func stop() {
        guard isMonitoring else { return }
        
        isMonitoring = false
    }
}

CPU over 80%

This is Matrix IOS Caton monitoring Mentioned:

We also think that too high CPU may cause application to get stuck, so when the sub thread checks the status of the main thread, if it detects that the CPU usage is too high, it will capture the current thread snapshot and save it to a file. At present, wechat applications believe that the single core CPU takes up more than 80% of the total, which is too high.

In general, this method can not be used alone as a Caton monitoring, but it can work together with other methods like wechat Matrix.

In GCDFetchFeed, if the CPU is more than 80%, Dai Ming will also capture the function call stack. Here is the code:

#define CPUMONITORRATE 80

+ (void)updateCPU {
    thread_act_array_t threads;
    mach_msg_type_number_t threadCount = 0;
    const task_t thisTask = mach_task_self();
    kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
    if (kr != KERN_SUCCESS) {
        return;
    }
    for (int i = 0; i < threadCount; i++) {
        thread_info_data_t threadInfo;
        thread_basic_info_t threadBaseInfo;
        mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
        if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
            threadBaseInfo = (thread_basic_info_t)threadInfo;
            if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
                integer_t cpuUsage = threadBaseInfo->cpu_usage / 10;
                if (cpuUsage > CPUMONITORRATE) {
                    //Print and record stack when cup consumption is greater than the set value
                    NSString *reStr = smStackOfThread(threads[i]);
                    SMCallStackModel *model = [[SMCallStackModel alloc] init];
                    model.stackStr = reStr;
                    //In the record database
                    [[[SMLagDB shareInstance] increaseWithStackModel:model] subscribeNext:^(id x) {}];
//                    NSLog(@"CPU useage overload thread stack: \n%@",reStr);
                }
            }
        }
    }
}

Stack information of Caton method

When we get the time point of carton, we need to get the stack of carton immediately. There are two ways: one is to traverse the stack frame, and the implementation principle is referred to iOS get arbitrary thread call stack The code RCBacktrace is written in detail. Another way is to obtain the call stack of any thread through Signal processing. The code is written in backtrace swift, but this way is troublesome when debugging. The first way is recommended.

Published 39 original articles, won praise 8, visited 30000+
Private letter follow

Keywords: iOS Swift snapshot Database

Added by sx on Thu, 05 Mar 2020 06:14:00 +0200