The principles of caton monitoring discussed in this article are also based on Choreographer, so take a look at these two articles before you go on to make sure you understand them better:

Choreographer Work logic summary

Application Fluency (FPS) monitoring

This paper mainly refers to the implementation ideas of the caton monitoring from: FPSViewer, this paper mainly summarizes the implementation principle

How do I determine if an application is stalling?

From the previous article we know that the Choreographer.doFrame() method must complete within 16.67ms to not cause an application to lose frames, so a Choreographer.doframe () execution time exceeding 16.67ms is considered an application A deadlock has occurred, so how do you determine the execution time of Choreographer.doFrame()?

Calculates the execution time of Choreographer.doFrame()

Choreographer actually provides an API:postFrameCallback():

Choreographer.getInstance().postFrameCallback(object :Choreographer.FrameCallback{
    override fun doFrame(frameTimeNanos: Long) {
    
    }
})
Copy the code

This Callback will be called back in Choreographer.doFrame().

The frameTimeNanos parameter doFrame(frameTimeNanos: Long) above means: The frame really start point in time, so we can by computing two Choreographer. FrameCallback interval to deduce the Choreographer. DoFrame how long () is carried out, probably code is as follows:

Choreographer.getInstance().postFrameCallback(object :Choreographer.FrameCallback{
    override fun doFrame(frameTimeNanos: Long) {
        if(mLastFrameTimeNanos ! = 0L) {val diffFrameCoast = (frameTimeNanos - mLastFrameTimeNanosdoForEach {it. DoFrame (diffFrameCoast)}} mLastFrameTimeNanos = frameTimeNanos Choreographer.getInstance().postFrameCallback(this) } })Copy the code

As the cycle called Choreographer. GetInstance (). PostFrameCallback (this) (this will cause the Choreographer request Vsync signal back and forth continuously adjustable Choreographer. DoFrame () method). The execution time of the doFrame method is calculated and notified to the observer.

Capture card,

Now that we have the execution time for Choreographer.doFrame() above, we just need to define a stall threshold to determine if a stall has occurred:

class RabbitBlockMonitor : ChoreographerFrameUpdateMonitor.FrameUpdateListener {

    val blockThreshold = 16666666 * 10

    override fun doFrame(frameCostNs: Long) {

        if (frameCostNs > blockThreshold) {
            toastInThread("Detected Caton!!")}}}Copy the code

Above I simply set the detection threshold at 10 frames (10 * 16.67).

How do I capture the stack when stalling occurs?

There are two main problems with obtaining the stack:

  1. Getting the stack for the main thread can cause the main thread to stall
  2. Getting a stack at a point in time is just a snapshot of the stack at that point in time, so how do you get a stack at a previous time when a freeze occurs?

For the first problem above we can get the main thread stack in the child thread. , but it is more difficult to get the past stack at the current moment. Here we take another approach: periodically collecting the main thread stack

Periodically collect the main thread stack

We can retrieve the main thread’s stack every 16.67ms from the asynchronous thread and store it, treating the stack as if it were stuck. This method captures most of the stalling scene, but it also captures some useless stack information.

Let’s look at the concrete implementation method:

class RabbitBlockMonitor : ChoreographerFrameUpdateMonitor.FrameUpdateListener {

    private var stackCollectHandler: Handler? = null
    private var blockStackTraces = HashMap<String, RabbitBlockStackTraceInfo>()

    private val blockStackCollectTask = object : Runnable {
        override fun run() {
            val stackTrace =  RabbitBlockStackTraceInfo(getUiThreadStackTrace())
            val mapKey = stackTrace.getMapKey()
            val info = blockStackTraces[mapKey]
            blockStackTraces[mapKey] = if (info == null) stackTrace elseinfo.collectCount++ stackCollectHandler? .postDelayed(this,stackCollectPeriod) } } init { val sampleThread = HandlerThread("rabbit_block_monitor")
        sampleThread.start()
        stackCollectHandler = Handler(sampleThread.looper)
    }

    override fun doFrame(frameCostNs: Long) { stackCollectHandler? .removeCallbacks(blockStackCollectTask)if(frameCostNs > blockThreshold && blockStackTraces.isNotEmpty()) { ... Save stack information} stackCollectHandler? .postDelayed(blockStackCollectTask, stackCollectPeriod) blockStackTraces.clear() } }Copy the code

Let’s take a look at the main logic of the code above:

  1. inChoreographer.doFrame()The callback is passedHandlertoHandlerThreadSend a message.
  2. inHandlerThreadTo collect the main thread stack and save it in the map (avoid collecting duplicate stack information)
  3. With a fixed stack acquisition cycle directionHandlerThreadContinue sending stack-collected messages
  4. whenChoreographer.doFrame()theframeCostNsWhen the caton threshold is exceeded, theframeCostNsThis time the stack is collected as the Caton scene

Note that the above code does not collect the main thread stack when frameCostNs < stackCollectPeriod.

In fact, the principle of stack collection is roughly as follows:

That is, if the above is stuck for 200ms, then the stack will be collected 11 times.

This gives a general overview of how caton monitors work, but there are some details that I won’t cover in this article. If you want to know more, you can see the Rabbit implementation.

The final capture of the stack scene looks something like this:

Rabbit is a performance monitoring project I’ve been working on in the field recently. I’ll be Posting more performance monitoring posts later. Welcome to follow my wechat official account: