Skip to content

All about Android Startups Metrics

Published: at 06:27 AM
Modified: (6 min read)

TLDR;

So agenda for great cold startup should be

What defines an AppStart at Higher Level

At a higher level we expect following things

image

Which is

What actually happens

This UML diagram is taken from Py’s dev.to Blog, link in reference below, highly recommended to read it

image

Now everything in sequence

So from icon click to first draw all these things happens.

What comes under Cold start?

The user experience of launching an activity starts when the user touches the screen, however app developers have little influence on the time spent before ActivityThread.handleBindApplication(), so that’s where app cold start monitoring should start.

So we should monitor from handleBindApplication till first draw.

To measure it

class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    var firstActivityCreated = false

    registerActivityLifecycleCallbacks(object :
        ActivityLifecycleCallbacks {

      override fun onActivityCreated(
          activity: Activity,
          savedInstanceState: Bundle?
      ) {
        if (firstActivityCreated) {
          return
        }
        firstActivityCreated = true
      }
    })
    Handler().post {
      if (firstActivityCreated) {
        // TODO Report cold start
      }
    }
  }
}

Idea

Lukewarm Start

There are many potential states that could be considered warm starts. For instance:

  • The user backs out of your app, but then re-launches it. The process may have continued to run, but the app must recreate the activity from scratch via a call to onCreate().
  • The system evicts your app from memory, and then the user re-launches it. The process and the activity need to be restarted, but the task can benefit somewhat from the saved instance state bundle passed into onCreate().

So if the activity is created with a saved instance state bundle, then that shouldn’t be considered a cold start. However, since the process needs to be restarted, there’s still a lot more work to do than just creating an activity. Let’s call this a lukewarm start.

class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    var firstActivityCreated = false
    var hasSavedState = false

    registerActivityLifecycleCallbacks(object :
        ActivityLifecycleCallbacks {

      override fun onActivityCreated(
          activity: Activity,
          savedInstanceState: Bundle?
      ) {
        if (firstActivityCreated) {
          return
        }
        firstActivityCreated = true
        hasSavedState = savedInstanceState != null
      }
    })
    Handler().post {
      if (firstActivityCreated) {
        if (hasSavedState) {
          // TODO Report lukewarm start
        } else {
          // TODO Report cold start
        }
      }
    }
  }
}

Check if its a cold start programatically?

Sometimes it would be useful to know that from within Application.onCreate(). For example, we might want to preload resources asynchronously to optimize cold start:

class MyApp : Application() {
  override fun onCreate() {
    super.onCreate()
    if (isColdStart()) {
      preloadDataForUiAsync()
    }
  }
}

We can also use activity importance to double check on coldStart hypothesis. Taken from Why Did my app start? blog of Py

So final code looks like this

class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()
    if (isForegroundProcess()) {
      var firstPostEnqueued = true
      Handler().post {
        firstPostEnqueued = false
      }
      registerActivityLifecycleCallbacks(object :
          ActivityLifecycleCallbacks {
        override fun onActivityCreated(
            activity: Activity,
            savedInstanceState: Bundle?
        ) {
          unregisterActivityLifecycleCallbacks(this)
          if (firstPostEnqueued && savedInstanceState == null) {
            // TODO Report cold start
          }
        }
      })
    }
  }

  private fun isForegroundProcess(): Boolean {
    val processInfo = ActivityManager.RunningAppProcessInfo()
    ActivityManager.getMyMemoryState(processInfo)
    return processInfo.importance == IMPORTANCE_FOREGROUND
  }
}

Neat,

and final end time is like following

class MyApp : Application() {
  var firstFrameDoneMs: Long = 0
  override fun onCreate() {
    super.onCreate()
    Choreographer.getInstance().postFrameCallback {
      firstFrameDoneMs = SystemClock.uptimeMillis()
    }
  }
}

Trying all this in real App

To understand things better lets run this on an existing android application

Will be running benchmarks on this app called Meme Generator on my github.

We would be using benchmark library and method trace visualizer here.

Without Application class

To start with the application does not have app class. And following are benchmark results

Starting 1 tests on moto g45 5G - 15

ExampleStartupBenchmark_startup
timeToInitialDisplayMs   min 456.6,   median 527.4,   max 639.4
Traces: Iteration 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

Pictorial representation

image

So, android studio profiler feature was used to get .trace file, and then used JAR from here to see this above image.

Above screenshot does not show app process creation brick, this is because application class was not created and attached to App in manifest file.

Always do sampling to create get small .trace file like in above Screenshot

image

With application class

class MemeApp: Application() {

    override fun onCreate() {
        super.onCreate()
        initFirebase()
        initAnalytics()
    }

    private fun initAnalytics() {
        Thread.sleep(500)
    }

    private fun initFirebase() {
        Thread.sleep(2000)
    }

}

Now to see benchmark results we have

Starting 1 tests on moto g45 5G - 15

ExampleStartupBenchmark_startup
timeToInitialDisplayMs   min 2,929.6,   median 3,030.7,   max 3,047.4
Traces: Iteration 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

As expected median as bumped by roughly 2500 ms, i.e 2.5 seconds

Pictorial representation

image

Findings

Heavy work on activity’s onCreate

Earlier it took 18 ms for MainActivity’s onCreate image

Now, in MainActivity’s onCreate I am doing Thread.sleep(500), lets see this now

Starting 1 tests on moto g45 5G - 15

ExampleStartupBenchmark_startup
timeToInitialDisplayMs   min 3,522.8,   median 3,533.7,   max 3,580.1
Traces: Iteration 0 1 2 3 4

image

as expected onCreate time impacted and hence first draw took time

Even if we put delay from onCreate to onResume() still metric would be same, because sequence to first draw is following

Application.onCreate()

Activity.onCreate()

Activity.onStart()

Activity.onResume()

Layout inflation, View.draw(), doFrame, etc.

So agenda for great cold startup should be

Stay tuned for more startup insights!

Resource

Articles by Py here, its a 11 part series, must go through them for details