Blog Infos
Author
Published
Topics
, , , ,
Published
Photo by CARTIST on Unsplash

 

The user’s initial impression comes from app start up. Google’s research discusses the RAIL (Response, Animation, Idle, Load) model, which emphasizes that users expect web and mobile experiences to load within 1–2 seconds for optimal user satisfaction. But, as business grows and development continues, it is very common that app becomes slower. That’s why it’s good practice to optimize code regularly after couple of months.

First, make it work. Then, make it right. Finally, make it fast. — Kent Beck

Baseline Profiles help improve app startup and runtime performance by up to 30% or more by providing precompiled code paths that reduce the need for JIT compilation and interpretation. We serve apk file which contain DEX files, which are basically bytecode representations of the app’s code. The Android Runtime uses JIT compilation to convert this bytecode into native code at runtime, compiling code only when it’s needed.
From Android 5.0, Google introduced Ahead of Time (AOT) compilation, which pre-compiles code during app installation to improve runtime performance. Android 7.0 further enhanced this with a hybrid approach, combining AOT, JIT, and Profile-Guided Optimizations to progressively improve performance based on how the user interacts with the app. But it takes some time to get improved performance for most used functions.
Baseline profile helps here.

Baseline Profiles helps here by providing a predefined set of optimized code paths with the APK. This allows the AOT compilation of frequently used code paths during app installation or the first launch, significantly improving the app’s runtime performance right from the start.

Startup Profile

Startup profile is a part of Baseline profile which focuses on improving app start up. Startup profile mainly organizes and prioritizes the code within the DEX files to ensure that the code required for the app’s startup is compiled and executed more efficiently. This re-organization reduces the time needed to compile and interpret the necessary code paths, leading to a faster app launch.

Code locality improvement from DEX layout optimization from official doc

 

Simulating slow app startup is kind of hard for an example project. Practically there can have dozens of reasons for slow start up. If you understand writings above correctly, you already know if something blocks main thread for 2 seconds, it will still block main thread for 2 seconds even after adding Startup Profile. I found running long for loop helps to simulate slow start up. But again, following example creates controller environment where Startup Profile performs well. In practical cases, it will definitely work different.

Requirements:

  • Android Gradle Plugin 8.2 or higher
  • Android Studio Iguana or higher
  • Jetpack Macrobenchmark 1.2.0 or higher
  • Should have DEX layout optimizations enabled.

Let’s look into following application class

class MyApp : Application() {
    override fun onCreate() {
        doHeavyComputationOnMainThread()
        super.onCreate()
    }

    // A method that performs heavy computation on the main thread
    private fun doHeavyComputationOnMainThread() {
        var sum = 0L
        for (i in 1..500_000_000) {
            for (j in 1..5) {
                sum += i + j
            }
            if (i % 100_000_000 == 0) {
                println("Sum at $i iterations: $sum")
            }
        }
        println("Final sum: $sum")
    }
}

In the application class, a long nested loop runs before finishing onCreate method call. This will take a significant amount of time to finish the startup and showing up the luncher activity.

Setting Up Baseline Profiles

To setup Baseline profile, we can use Android Studio’s Baseline Profile Generator from template. To do that, Go to File > New > New Module.. , or right click on the project and select New > Module.

Then select Baseline Profile Generator from the appeared window, set a name of the module and click finish.

It will create a module for the Baseline Profile and, it will automatically apply the following changes in app build.gradle:

plugins {
  id("androidx.baselineprofile")
}

dependencies {
  // ...
  implementation("androidx.profileinstaller:profileinstaller:1.3.1")
  "baselineProfile"(project(mapOf("path" to ":baselineprofile")))
}

Also make sure we have Dex layout optimization enabled

android {
  // ...
  baselineProfile {
      dexLayoutOptimization = true
  }
}

Generating the Baseline Profile

In the generated module, there will be default baseline generator code. For the example application code above, the generated code will be good enough. The generated code will looks like the following:

@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {

    @get:Rule
    val baselineProfileRule = BaselineProfileRule()

    @Test
    fun generateBaselineProfile() {
        // The package name of your app
        val targetAppId = "com.example.slowappstartup"  // <-- Change to your package name

        // Collect baseline profile
        baselineProfileRule.collect(
            packageName = targetAppId,
            maxIterations = 10,
            stableIterations = 10,
            includeInStartupProfile = true // Include the startup profile
        ) {
            pressHome() // Ensure cold start by pressing home before starting the app
            startActivityAndWait() // Start the main activity of the app
        }
    }
}

It’s important to have includeInStartupProfile = true in the app startup Critical User Journey (CUJ) rule. This will just open default luncher activity after startup.

Adapting for Complex Apps: As the main target of the Baseline Profile is to improve the most frequent user flows, for more complex apps this will be different. For example, users usually go through the login flow once. Once logged in, the user navigates to the home page for a long period of time until they log out. In this scenario, launching the home screen after startup will be the most frequent user flow. To improve this flow, we will need to modify the profile generator code accordingly.

To generate the Baseline Profile, run the :app:generateBaselineProfile or :app:generateVariantBaselineProfile task. Alternatively, the easiest way is by clicking on run button beside the Baseline Profile generator class.

This will create startup profile in a Human Readable Format (HRF) text file at the src/<variant>/generated/baselineProfiles/startup-prof.txt path.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Unclutter your Jetpack – an opinionated look at Googles library collection

Remember when Google hit the Reset button to clean up the mess of its Android support libraries? Since then, a ton of new Jetpack libraries have been created. Some are obvious choices, like compose.
Watch Video

Unclutter your Jetpack - an opinionated look at Googles library collection

Thomas Künneth
Senior Android Developer
snappmobile_io

Unclutter your Jetpack - an opinionated look at Googles library collection

Thomas Künneth
Senior Android Devel ...
snappmobile_io

Unclutter your Jetpack - an opinionated look at Googles library collection

Thomas Künneth
Senior Android Developer
snappmobile_io

Jobs

Measuring the Impact with Macrobenchmark

The impact of Startup Profiling can be measured using Macrobenchmark.

Macrobenchmark: Macrobenchmark measures the performance of large, end-to-end operations in our app, such as app startup time or scrolling through a RecyclerView. When we need to assess the performance of complex user interactions or app flows involving multiple components, we use Macrobenchmark.
Microbenchmark: Microbenchmark measures the performance of small, isolated pieces of code, like individual methods or algorithms. When we want to optimize hot code paths that are executed frequently, such as data processing functions or computational algorithms, we use Microbenchmark.

We chose macrobenchmarking because app startup is a complex process involving many systems and components. Macrobenchmarking allows us to measure the overall startup performance in a realistic environment, capturing the full impact on the user experience.

The code to measure performance looks like as follows:

@RunWith(AndroidJUnit4ClassRunner::class)
class StartupBenchmark {
    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun startupDefaultCompilation() = startup(CompilationMode.DEFAULT)

    @Test
    fun startupFullCompilation() = startup(CompilationMode.Full())

    private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
        packageName = "com.example.slowappstartup",
        metrics = listOf(StartupTimingMetric()),
        compilationMode = compilationMode,
        iterations = 10,
        startupMode = StartupMode.COLD,
        setupBlock = {
            pressHome()
        },
    ) {
        // Waits for the first rendered frame, which represents time to initial display.
        startActivityAndWait()
    }
}

Here, we are running two test cases to compare: one with the default compilation mode and another with full AOT compilation applying the Baseline Profile.

The behavior of CompilationMode.DEFAULT varies in different API Levels:

  • API 24 and Above (Android 7.0+):
  • Uses partial Ahead-of-Time (AOT) compilation with Baseline Profiles if available.
  • Baseline Profiles optimize critical code paths used during app startup and user interaction.
  • If Baseline Profiles are not available or fail to install, the app falls back to Just-In-Time (JIT) compilation without throwing an error.
  • Below API 24:
  • Uses full AOT compilation, where all apps are fully compiled ahead of time during installation.

Benchmark result is in the following

This benchmark result shows a significant app startup improvement after applying Startup Profile. But as mentioned earlier, the example works best for simulation. Usually reasons behind slow startup are completely different in real life apps. Practically, we will get 15% to 30% improvement in app startup.

Things to share…

  • Baseline Profile must run on rooted device or, emulators or, on Android 13 or higher without root access.
  • From AGP 8.0, Baseline Profile is enabled by default. There’s no way to turn off this. See android.enableArtProfiles here in release doc of AGP 8.0.
  • As Baseline Profile is enabled by default, I compared CompilationMode.DEFAULT with CompilationMode.Full() . Otherwise, it can be compared with CompilationMode.None() .
  • Startup Profile has effect in app size.
  • Again, write Baseline Profile for most frequent user flows.
  • Generate Baseline Profile for different types of devices for better performance for different hardwares.
  • Baseline Profile generation supports different configurations. Reading official doc to Configure Baseline Profile generation is a must.
  • Optimize your code regularly. Baseline Profiles will just help to further enhance performance, but they are not a substitute for efficient coding practices.
  • There are a lots of things to know about Baseline Profile. Even in official docs of Baseline Profile, Macorbenchmark, Microbenchmark, you will see more description and explanation rather than actual code. You must know root of these things to work on. I tried to ignore basic theory in the blog cause it’s already become a long blog.
Support Links:

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE
Menu