Lag Monitoring for iOS Performance Optimization -Swift
Recently, when I was doing performance optimization related content in the company, my leader asked me to set up the lag monitoring platform first, so I began to conduct research on the current mainstream lag monitoring scheme. There are three schemes I found on the Internet:
- Monitor screen FPS with CADisplayLink
- Ping the main thread through the child thread
- By monitoring the status of RunLoop
Let’s break it down one by one. Since the company project is Swift pure, the following code is Swift pure.
For more information on the principles of screen imaging and the causes of stacken, please refer to my other article: OC Underlying Exploration – Performance Optimization
The code in the article: CatonMonitor
CADisplayLink
CADisplayLink is a timer class that allows us to draw specific content onto the screen at a rate that keeps pace with the refresh rate of the screen. After CADisplayLink registers with Runloop in a specific mode, the Runloop sends the specified selector message to CADisplayLink’s target every time the screen refreshes. The corresponding selector of the CADisplayLink class is called once.
CADisplayLink is good for continuous redrawing of the interface, such as the need to grab the next frame for rendering while a video is playing.
FPS (Frames Per Second) is the definition in the image field, which refers to the number of Frames rendered Per Second and is usually used to measure the smoothness of the picture. The more Frames Per Second, the smoother the picture will be. 60fps is the best.
Monitor screen FPS with CADisplayLink by adding a commonMode CADisplayLink to the main thread RunLoop, and executing CADisplayLink at the end of each screen refresh. So you can count the number of screen refreshes in 1s, which is the FPS.
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
}
}
// Suitable for monitoring UI lag, not suitable for monitoring CPU lag caused by a sudden excessively high
class FPSLabel: UILabel {
var link: CADisplayLink!
// Count the number of times the method is executed
var count: Int = 0
// Record the time when the last method was executed, using link.timestamp - lastTime to calculate the interval
var lastTime: TimeInterval = 0
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.backgroundColor = UIColor.white.withAlphaComponent(0.7)
link = CADisplayLink(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.update(link:)))
link.add(to: RunLoop.main, forMode: .common)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")}@objc func update(link: CADisplayLink) {
guard lastTime ! = 0 else {
lastTime = link.timestamp
return
}
count + = 1
let timePassed = link.timestamp - lastTime
// The time is greater than or equal to 1 second, that is, the FPSLabel refresh interval, do not want to refresh too frequently
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(NSAttributedString.Key.foregroundColor, value: color, range: NSRange(location: 0, length: text.length - 3))
text.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.white, range: NSRange(location: text.length - 3, length: 3))
text.addAttribute(NSAttributedString.Key.font, value: UIFont(name: "Menlo", size: 14)!, range: NSRange(location: 0, length: text.length))
text.addAttribute(NSAttributedString.Key.font, value: UIFont(name: "Menlo", size: 14)!, range: NSRange(location: text.length - 4, length: 1))
self.attributedText = text
}
deinit {
link.invalidate()
}
}
Copy the code
However, simply monitoring the FPS makes it difficult to determine whether or not a stall is occurring, because there is no way to define the range within which a stall can be identified.
The child thread pings the main thread
The core idea of the scheme is to create a child thread to ping the main thread through the semaphore, set the flag bit to YES for each ping, and then send the task to the main thread, 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.
public class CatonMonitor {
static let shareInstance = CatonMonitor(a)private var isMonitoring = false
private let semaphore = DispatchSemaphore(value: 0)
public func start(a) {
if isMonitoring { return }
isMonitoring = true
DispatchQueue.global().async { [weak self] in
guard let strongSelf = self else {
return
}
while strongSelf.isMonitoring {
var timeout = true
DispatchQueue.main.async {
// If the main thread is not stuck, it will be executed
timeout = false
self?.semaphore.signal()
}
Thread.sleep(forTimeInterval: 0.05)
if timeout {
print("Stuck.")}self?.semaphore.wait()
}
}
}
public func stop(a) {
guard isMonitoring else { return }
isMonitoring = false
}
deinit {
stop()
}
}
Copy the code
Runloop
For more information on how Runloop works, check out my other article: OC Low-level Exploration – Runloop
The entire Runloop process can be summarized as follows:
- Into the loop
- Do while keep the thread alive
- Trigger the timer callback
- The source0 callback is triggered
- Perform block
- Enter the dormant
- Wait for a Mach port message
- Port-based source events
- The Timer to time
- Runloop timeout
- The caller wakes up
- Wake up the
- Process the message
- Check whether to enter the next loop
The purpose of RunLoop is to keep the thread busy when there are events to be processed and to put it to sleep when there are no events to be processed. If the thread of the RunLoop takes too long to execute the pre-sleep method and is unable to go to sleep, or if the thread wakes up and takes too long to receive messages and is unable to proceed to the next step, the thread is considered blocked. If the thread is the main thread, it will appear to be stuck.
So if we’re going to use the RunLoop principle to monitor Caton, we’re going to focus on these two phases. The two loop states defined by RunLoop before and after sleep are kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting. That is, both the Source0 callback and the mach_port message are triggered.
The core idea of the scheme is to open a child thread, and then calculate whether the time between the two states of Runloop kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting exceeds a certain threshold in real time to determine the main thread’s delay.
class RunLoopMonitor {
static let shareInstance = RunLoopMonitor(a)var timeoutCount = 0
var runloopObserver: CFRunLoopObserver?
var runLoopActivity: CFRunLoopActivity?
var dispatchSemaphore: DispatchSemaphore?
private init(a) {}
func start(a) {
guard runloopObserver = = nil else {
return
}
let uptr = Unmanaged.passRetained(self).toOpaque()
let vptr = UnsafeMutableRawPointer(uptr)
var content = CFRunLoopObserverContext(version: 0, info: vptr, retain: nil, release: nil, copyDescription: nil)
// Create an observer
runloopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true.0, observeCallBack(), &content)
// Add the observer to the common mode of the main thread runloop
CFRunLoopAddObserver(CFRunLoopGetMain(), runloopObserver, CFRunLoopMode.commonModes)
// Initialize the semaphore to 0
dispatchSemaphore = DispatchSemaphore(value:0)
DispatchQueue.global().async {
// The child thread opens a continuous loop for monitoring
while(true) {
// The wait time is 250 milliseconds
let st = self.dispatchSemaphore?.wait(timeout:DispatchTime.now() + .milliseconds(50))
// st ==. TimeOut If a timeOut occurs, a delay occurs
if st = = .timedOut {
if self.runloopObserver = = nil {
self.dispatchSemaphore = nil
self.runLoopActivity = nil
self.timeoutCount = 0
return
}
// BeforeSources and AfterWaiting are two states that can detect delays
if self.runLoopActivity = = .afterWaiting || self.runLoopActivity = = .beforeSources {
// Three times in a row
self.timeoutCount + = 1
if self.timeoutCount < 3 {
continue
}
DispatchQueue.global().async {
print("Stuck.")
// Capture the stack for reporting}}}self.timeoutCount = 0}}}func end(a) {
guard let _ = runloopObserver else {
return
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runloopObserver, CFRunLoopMode.commonModes)
runloopObserver = nil
}
deinit {
end()
}
private func observeCallBack(a) -> CFRunLoopObserverCallBack {
return{ (observer, activity, context)in
let weakSelf = Unmanaged<RunLoopMonitor>.fromOpaque(context!).takeUnretainedValue()
// Get the current activity
weakSelf.runLoopActivity = activity
// Add semaphore after completing an observation
weakSelf.dispatchSemaphore?.signal()
}
}
}
Copy the code
The CPU exceeds 80%
This is what the Matrix-ios Caton monitor says:
We also believe that too high CPU may also cause application lag, so when the child thread checks the status of the main thread, if it detects that the CPU usage is too high, it will take a snapshot of the current thread and save it in a file. At present, it is considered in wechat application that the single-core CPU usage exceeds 80%, which means that the CPU usage is too high.
This method generally cannot be used alone as caton monitoring, but it can work together with other methods just like wechat Matrix.
Daming made the implementation in SMCPUMonitor, the repository also has the code to collect the stack information of the card.
// Polling checks for multiple threads of CPU
+ (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 > 70) {
// The stack is printed and logged when the CPU consumption is greater than 70
NSString *reStr = smStackOfThread(threads[i]);
// Record the database
[[[SMLagDB shareInstance] increaseWithStackString:reStr] subscribeNext:^(id x) {}];
NSLog(@"CPU useage overload thread stack:\n% @",reStr);
}
}
}
}
}
Copy the code
Wechat Matrix optimization
In wechat Matrix tool, in order to reduce performance loss caused by detection, we added annealing algorithm for detection thread:
- Each time a child thread detects that the main thread is stuck, it retrieves the main thread stack and stores it in memory (it does not take a snapshot of the thread directly to store it in a file).
- Compare the main thread stack obtained with the main thread stack obtained by caton last time:
- If the stack is different, a snapshot of the current thread is taken and written to the file;
- If they are the same, it skips and increments the check time according to the Fibonacci sequence until there is no lag or the main thread is stuck in a different stack.
In this way, the same card can avoid writing multiple files; Avoid writing thread snapshot files repeatedly when the main thread is stuck.
summary
In summary, it is recommended to use runloop to monitor the lag. Please look forward to the extraction of thread stack information about the lag
Update 29 June 2021: Swift stack information retrieval
Refer to the article
Summary of iOS caton monitoring scheme
Matrix-ios Caton monitoring
IOS development master class | how to utilize the RunLoop principle to monitor the caton?