Working Here
Engineering

Diagnosing Android app launch performance

Sri

Sri

 

April 5, 2022

Diagnosing Android app launch performance

In the mobile application world, studies have shown that 60% of users abandon the application if they face any performance-related issues. Having a slow startup can be enough to lose your users before they even get started. The low availability of resources makes it crucial to monitor and improve performance to optimize launch time and provide users with the best possible experience. Moreover, the Play Store also tends to rank apps with healthier vitals (startup time included) higher and will appear more often in search results.

During my time as a co-op developer on the Android Platform team, I worked on hunting down performance pain points during start-up of the app. The key was to find operations that block the application’s main thread which prevent it from initializing activities and drawing the UI. Potential bottlenecks on the main thread include:

  • Network Calls (API Requests, analytics, third party libraries etc.)  
  • Disk I/O operations (Room, SharedPreferences etc.)  
  • Initialization of large objects  
  • CPU-Intensive calculations (Image processing etc.)
  • Deadlock

Once these problems have been identified within the app, steps can be taken to resolve or mitigate the impact of each one. Some common solutions to resolve the issues include:  

  • Moving the operation to a background thread/co-routine  
  • Lazy initialization (recommended using a DI framework such as Hilt or Koin)  
  • Using callbacks instead of blocking the main thread

The question then becomes, how can you find these issues to optimize app performance? Between your code base and third-party libraries, there’s a lot of ground to cover. Fortunately, that’s where tools like method tracing and the Jetpack Macrobenchmark library come in handy.  

Method Tracing

Method tracing is one of the most effective ways to pinpoint slow points in the app, and gives you the control to measure specific portions of the app. To generate a method trace of your app's execution, you can instrument your app using the Debug class.

To create trace logs:

  1. Call Debug.startMethodTracing() where you want the system to start logging trace data. You can optionally specify the file name and the max file size.  
  1. To stop tracing, simply call Debug.stopMethodTracing() at a later point in the code.
  1. The resulting trace file can be found within your app’s storage directory (usually under sdcard/Android/data/<package-name>/files) and can be viewed directly within Android Studio’s Profiler.

The trace file will show the duration of the methods and native functions that are executed on every thread in the app process. To analyze startup, I start tracing at the start of the Application onCreate, and end finish the tracing at the end of the MainActivity onCreate. The resulting trace file can get pretty large, so I recommend setting the max file size to at least 100 MB.  

By analyzing the main thread, it becomes much easier to point out methods that are taking a while during application or the initial activity’s onCreate that are slowing down app startup. Let’s look at a sample from the TextNow app:  

When analyzing TextNow’s Application subclass, I noticed that a significant portion of startup (~1s on a debug build) was dedicated to simply setting up the Koin Dependency graph. While this number is inflated due to the overhead of tracing each method, the data coming from production indicated that on average, ~100 ms was spent on Koin initialization. After some research, it turns out that Koin builds its dependency graph during app startup. When we have a large number of dependencies, this can take a fair amount of time. One possible solution I investigated was switching to a DI framework such as Hilt, which does the heavy lifting at compile time instead of during app launch.  

Another case where traces helped find performance issues was a Disk I/O operation that was being performed numerous times during startup and the app’s lifetime.  

The above trace is one of many occurrences of the getUserName being called on the main thread. The function calls the Vessel database (TextNow’s open-source replacement for SharedPreferences) and waits for the result.  

One possible solution was to put this operation into a co-routine, however, in this case, the username was needed before proceeding further, and rearchitecting the activity was difficult. So, I decided to introduce an optional in-memory cache for the vessel database that makes retrieval from the database very cheap.  You can check out the feature here.

Jetpack Macrobenchmark

As part of the new AndroidX libraries, the Android team introduced the Jetpack Macrobenchmark library in order to provide an easy way to set up benchmarks for larger events like application startup. It allows you to create benchmarks for different startup modes as well as different levels of pre-compilation.  

As a bonus, it provides a trace log for each iteration. Although less detailed than using method tracing, it can give you details into higher level slow points in your application. Even though Macrobenchmark provides better consistency while benchmarking performance, the best data comes from production and from your users, which should ultimately be your source of truth.  

Pitfalls

Finding performance issues and measuring app startup times is never cut and dry. Here are some things to watch out for and consider:

  • Different devices: Be sure to test on a variety of different devices since each one may perform very differently.
  • Debug Mode: Performing method traces and debug builds in general will always produce much slower results than the release version of the app. Although the analysis comes in handy to measure relative performance, it is not a good measure of absolute performance.
  • Start-up modes: Cold start vs Warm Start can offer a very different view of the startup process. When apps are cold started, the OS has not loaded any of the app into memory yet and has to create the process from scratch, whereas a warm start may be the result of a user navigating back to the app after being away briefly.

Final Thoughts

Finding performance issues can be difficult and time-consuming, believe me. However, the effort is definitely worth it for a better customer experience and higher ratings on the Play Store.

If you would like to join Sri on his quest to get the fastest Android app start times, along with the rest of the team here at TextNow, check out our open positions!

Related posts