Android Vitals - First draw time 👩‍🎨
Tags: android, vitals, performance, monitoring
Published: 2020-08-29 11:51:28

> Header image: *Light Field* [by Romain Guy](https://www.flickr.com/photos/romainguy/12143262833/).

This blog series is focused on stability and performance monitoring of Android apps in production. Last week, I wrote about [how to best determine the app start time](https://dev.to/pyricau/android-vitals-when-did-my-app-start-24p4).

Today, we focus on determining the **time at which cold start ends**.

According to the [Play Console documentation](https://support.google.com/googleplay/android-developer/answer/7385505):

> Startup times are tracked when the app's **first frame completely loads**.

We learn a bit more from the [App startup cold time documentation](https://developer.android.com/topic/performance/vitals/launch-time#cold):

> Once the app process has **completed the first draw**, the system process swaps out the currently displayed background window, replacing it with the main activity. At this point, the user can start using the app.

In [Android Vitals - Rising to the first drawn surface 🤽‍♂️](https://dev.to/pyricau/android-vitals-rising-to-the-first-drawn-surface-1j9e), we learnt that:

* [ActivityThread.handleResumeActivity()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=4269;drc=69e700c7922c34583bddbab4b7ce78adadb90730) schedules the first frame.
* On the first frame [Choreographer.doFrame()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/Choreographer.java;l=662;drc=69e700c7922c34583bddbab4b7ce78adadb90730) calls [ViewRootImpl.doTraversal()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java;l=1736;drc=69e700c7922c34583bddbab4b7ce78adadb90730) which performs a measure pass, a layout pass, and finally the **first draw pass** on the view hierarchy.

![First draw](https://dev-to-uploads.s3.amazonaws.com/i/2ken9max4z9flkwlmdmf.png)

# First frame

Since API level 16, Android provides a simple API to schedule a callback when the next frame happens: [Choreographer.postFrameCallback()](https://developer.android.com/reference/android/view/Choreographer#postFrameCallback(android.view.Choreographer.FrameCallback)).

```kotlin
class MyApp : Application() {

  var firstFrameDoneMs: Long = 0

  override fun onCreate() {
    super.onCreate()
    Choreographer.getInstance().postFrameCallback {
      firstFrameDoneMs = SystemClock.uptimeMillis()
    }
  }
}
```

Unfortunately, calling `Choreographer.postFrameCallback()` has the side effect of scheduling a frame that runs **before the first traversal is scheduled**. So the time reported here is before the time of the frame that runs the first draw. I was able to reproduce this on API 25 but also noticed it doesn't happen in API 30, so this bug was probably fixed.

# First draw

## ViewTreeObserver

On Android, each view hierarchy has a [ViewTreeObserver](https://developer.android.com/reference/android/view/View#getViewTreeObserver()) which can hold callbacks for global events such as layout or draw.

### ViewTreeObserver.addOnDrawListener()

We can call [ViewTreeObserver.addOnDrawListener()](https://developer.android.com/reference/android/view/ViewTreeObserver#addOnDrawListener(android.view.ViewTreeObserver.OnDrawListener)) to register a draw listener:

```kotlin
view.viewTreeObserver.addOnDrawListener { 
  // report first draw
}
```

### ViewTreeObserver.removeOnDrawListener()

We only care about the first draw, so we need to remove the [OnDrawListener](https://developer.android.com/reference/android/view/ViewTreeObserver.OnDrawListener) as soon as we've received a callback. Unfortunately, [ViewTreeObserver.removeOnDrawListener()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewTreeObserver.java;l=739-754;drc=69e700c7922c34583bddbab4b7ce78adadb90730) cannot be called from the `onDraw()` callback:

```java
public final class ViewTreeObserver {
  public void removeOnDrawListener(OnDrawListener victim) {
    checkIsAlive();
    if (mInDispatchOnDraw) {
      throw new IllegalStateException(
          "Cannot call removeOnDrawListener inside of onDraw");
    }
    mOnDrawListeners.remove(victim);
  }
}
```

So we have to do the removal in a post:

```kotlin
class NextDrawListener(
  val view: View,
  val onDrawCallback: () -> Unit
) : OnDrawListener {

  val handler = Handler(Looper.getMainLooper())
  var invoked = false

  override fun onDraw() {
    if (invoked) return
    invoked = true
    onDrawCallback()
    handler.post {
      if (view.viewTreeObserver.isAlive) {
        viewTreeObserver.removeOnDrawListener(this)
      }
    }
  }

  companion object {
    fun View.onNextDraw(onDrawCallback: () -> Unit) {
      viewTreeObserver.addOnDrawListener(
        NextDrawListener(this, onDrawCallback)
      )
    }
  }
}
```

Notice the nice extension function:

```kotlin
view.onNextDraw { 
  // report first draw
}
```

### FloatingTreeObserver

If we call [View.getViewTreeObserver()](https://developer.android.com/reference/android/view/View#getViewTreeObserver()) before the view hierarchy is attached, there is no real ViewTreeObserver already available so the view will create [a fake one](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/View.java;l=24281-24288;drc=69e700c7922c34583bddbab4b7ce78adadb90730) to store the callbacks:

```kotlin
public class View {
  public ViewTreeObserver getViewTreeObserver() {
    if (mAttachInfo != null) {
      return mAttachInfo.mTreeObserver;
    }
    if (mFloatingTreeObserver == null) {
      mFloatingTreeObserver = new ViewTreeObserver(mContext);
    }
    return mFloatingTreeObserver;
  }
}
```

Then when the view is attached the callbacks are [merged back into the real ViewTreeObserver](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/View.java;l=20093-20096;drc=69e700c7922c34583bddbab4b7ce78adadb90730?q=View.java&ss=android%2Fplatform%2Fsuperproject).

That's nice, except there was a bug [fixed in API 26](https://android.googlesource.com/platform/frameworks/base/+/9f8ec54244a5e0343b9748db3329733f259604f3): the draw listeners were not merged back into the real view tree observer.

We work around that by waiting until the view is attached before registering our draw listeners:

```kotlin
class NextDrawListener(
  val view: View,
  val onDrawCallback: () -> Unit
) : OnDrawListener {

  val handler = Handler(Looper.getMainLooper())
  var invoked = false

  override fun onDraw() {
    if (invoked) return
    invoked = true
    onDrawCallback()
    handler.post {
      if (view.viewTreeObserver.isAlive) {
        viewTreeObserver.removeOnDrawListener(this)
      }
    }
  }

  companion object {
    fun View.onNextDraw(onDrawCallback: () -> Unit) {
      if (viewTreeObserver.isAlive && isAttachedToWindow) {
        addNextDrawListener(onDrawCallback)
      } else {
        // Wait until attached
        addOnAttachStateChangeListener(
            object : OnAttachStateChangeListener {
          override fun onViewAttachedToWindow(v: View) {
            addNextDrawListener(onDrawCallback)
            removeOnAttachStateChangeListener(this)
          }

          override fun onViewDetachedFromWindow(v: View) = Unit
        })
      }
    }

    private fun View.addNextDrawListener(callback: () -> Unit) {
      viewTreeObserver.addOnDrawListener(
        NextDrawListener(this, callback)
      )
    }
  }
}
```

## DecorView

Now that we have a nice utility to listen to the next draw, we can use it when an activity is created. Note that the first created activity may not draw: it's fairly common for apps to have a **trampoline activity** as launcher activity which immediately starts another activity and finishes itself. We register our draw listener on the activity window [DecorView](https://developer.android.com/reference/android/view/Window#getDecorView()).

```kotlin
class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    var firstDraw = false

    registerActivityLifecycleCallbacks(
      object : ActivityLifecycleCallbacks {
      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        if (firstDraw) return
        activity.window.decorView.onNextDraw {
          if (firstDraw) return
          firstDraw = true
          // report first draw
        }
      }
    })
  }
}
```

### Locking window characteristics

According to the documentation for [Window.getDecorView()](https://developer.android.com/reference/android/view/Window#getDecorView()):

> Note that calling this function for the first time "locks in" various window characteristics as described in setContentView().

Unfortunately, we're calling `Window.getDecorView()` from [ActivityLifecycleCallbacks.onActivityCreated()](https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks?hl=en#onActivityCreated(android.app.Activity,%20android.os.Bundle)) which is [called by Activity.onCreate()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Activity.java;l=1479;drc=1c7cc22adf0d1e5a405f1ad56e2ae73912c2fee3). In a typical activity, `setContentView()` is called **after** `super.onCreate()` so we're calling `Window.getDecorView()` before `setContentView()` is called, which has unexpected side effects.

We need to wait for `setContentView()` to be called before we retrieve the decor view.

### Window.Callback.onContentChanged()

We can use [Window.peekDecorView()](https://developer.android.com/reference/android/view/Window#peekDecorView()) to determine if we already have a decor view. If not, we can register a callback on our window, which provides the hook we need, [Window.Callback.onContentChanged()](https://developer.android.com/reference/android/view/Window.Callback#onContentChanged()):

> This hook is called whenever the content view of the screen changes (due to a call to Window#setContentView() or Window#addContentView()).

However, a window can only have one callback, and the activity already [sets itself](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Activity.java;l=7736;drc=1c7cc22adf0d1e5a405f1ad56e2ae73912c2fee3) as the window callback. So we'll need to replace that callback and delegate to it.

Here's a utility class which does that and adds a `Window.onDecorViewReady()` extension function:

```kotlin
class WindowDelegateCallback constructor(
  private val delegate: Window.Callback
) : Window.Callback by delegate {

  val onContentChangedCallbacks = mutableListOf<() -> Boolean>()

  override fun onContentChanged() {
    onContentChangedCallbacks.removeAll { callback ->
      !callback()
    }
    delegate.onContentChanged()
  }

  companion object {
    fun Window.onDecorViewReady(callback: () -> Unit) {
      if (peekDecorView() == null) {
        onContentChanged {
          callback()
          return@onContentChanged false
        }
      } else {
        callback()
      }
    }

    fun Window.onContentChanged(block: () -> Boolean) {
      val callback = wrapCallback()
      callback.onContentChangedCallbacks += block
    }

    private fun Window.wrapCallback(): WindowDelegateCallback {
      val currentCallback = callback
      return if (currentCallback is WindowDelegateCallback) {
        currentCallback
      } else {
        val newCallback = WindowDelegateCallback(currentCallback)
        callback = newCallback
        newCallback
      }
    }
  }
}
```

### Leveraging `Window.onDecorViewReady()`

Let's put it all together:

```kotlin
class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    var firstDraw = false

    registerActivityLifecycleCallbacks(
      object : ActivityLifecycleCallbacks {
      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        if (firstDraw) return
        val window = activity.window
        window.onDecorViewReady {
          window.decorView.onNextDraw {
            if (firstDraw) return
            firstDraw = true
            // report first draw
          }
        }
      }
    })
  }
}
```

## Not quite there yet

Let's look at the [OnDrawListener.onDraw()](https://developer.android.com/reference/android/view/ViewTreeObserver.OnDrawListener#onDraw()) documentation:

> Callback method to be invoked when the view tree is about to be drawn.

Drawing can still take a while. We want to know when the drawing is done, not when it starts. Unfortunately, there is no `ViewTreeObserver.OnPostDrawListener` API.

In [Android Vitals - Rising to the first drawn surface 🤽‍♂️](https://dev.to/pyricau/android-vitals-rising-to-the-first-drawn-surface-1j9e), we learnt that the first frame and traversal all happen in just one `MSG_DO_FRAME` message. If we could determine when that message ends, we would know when we're done drawing.

# Handler.postAtFrontOfQueue()

Instead of determining when the `MSG_DO_FRAME` message ends, we can detect when the next message starts by posting to the front of the message queue with [Handler.postAtFrontOfQueue()](https://developer.android.com/reference/android/os/Handler#postAtFrontOfQueue(java.lang.Runnable)):

```kotlin
class MyApp : Application() {

  var firstDrawMs: Long = 0

  override fun onCreate() {
    super.onCreate()

    var firstDraw = false
    val handler = Handler()

    registerActivityLifecycleCallbacks(
      object : ActivityLifecycleCallbacks {
      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        if (firstDraw) return
        val window = activity.window
        window.onDecorViewReady {
          window.decorView.onNextDraw {
            if (firstDraw) return
            firstDraw = true
            handler.postAtFrontOfQueue {
              firstDrawMs = SystemClock.uptimeMillis()
            }
          }
        }
      }
    })
  }
}
```

**Edit:** I measured the time difference between the first `onNextDraw()` and the following `postAtFrontOfQueue()` in production on a large number of devices, here are the results:

* 10th percentile: 25ms
* 25th percentile: 37ms
* 50th percentile: 61ms
* 75th percentile: 109ms
* 90th percentile: 194ms

That interval is significant enough to not be left out.

## Conclusion

We now have everything we need to monitor cold start times in production:

* In [Why did my process start? 🌄](https://dev.to/pyricau/android-vitals-why-did-my-process-start-4d0e) we learnt how to detect a cold start.
* In [When did my app start? ⏱](https://dev.to/pyricau/android-vitals-when-did-my-app-start-24p4) we learnt how to determine the app start time.
* In this blog we learnt how to determine the first draw time.

I hope you enjoyed these deep dives, stay tuned for more!

{% twitter 1291126907426099200 %}