Boosting Performance: Baseline Profiles in Jetpack Compose

In the world of Android development, optimizing application performance is paramount, especially when using modern UI toolkits like Jetpack Compose. One effective technique for performance optimization is leveraging Baseline Profiles. Baseline Profiles are a way to provide ahead-of-time (AOT) compilation hints to the Android Runtime (ART), improving application startup speed and reducing jank. This post will guide you on how to effectively use Baseline Profiles in a Jetpack Compose application to boost its performance.

What are Baseline Profiles?

Baseline Profiles are a set of class and method signatures that guide the Android Runtime (ART) compiler to pre-compile the key code paths in your application. This pre-compilation can result in significant performance improvements, particularly in startup time, jank reduction, and overall responsiveness. By including Baseline Profiles with your app, the system can optimize the code execution path during installation or background optimization, resulting in a smoother user experience.

Why Use Baseline Profiles?

  • Improved Startup Time: AOT compilation of critical code paths reduces the need for runtime interpretation and JIT (Just-In-Time) compilation during app startup.
  • Reduced Jank: Smoother animations and transitions due to pre-compiled code paths.
  • Better Performance Consistency: Reduces variability in performance across different devices and Android versions.
  • Enhanced User Experience: Provides a more responsive and fluid interaction with your application.

How to Implement Baseline Profiles in Jetpack Compose

Here’s a step-by-step guide on how to implement Baseline Profiles in your Jetpack Compose application.

Step 1: Add Required Dependencies

To generate and use Baseline Profiles, you need to add specific dependencies to your build.gradle file.


dependencies {
    // Baseline Profile dependencies
    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
    androidTestImplementation("androidx.benchmark:benchmark-macro-junit4:1.2.2")
}

Step 2: Configure the androidTest Source Set

Create a new module, usually named :baselineprofile, for generating baseline profiles. Set up its build.gradle file:


plugins {
    id("com.android.module")
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.example.baselineprofile" // Replace with your app's namespace
    compileSdk = 34

    defaultConfig {
        minSdk = 24
        targetSdk = 34

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    
    testOptions {
        animationsDisabled = true
    }
}

dependencies {
    implementation(project(":app"))  // Assuming your main app module is named 'app'
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

    androidTestImplementation("androidx.benchmark:benchmark-macro-junit4:1.2.2")
    androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0") // Required for interacting with the UI
}

Update your project’s settings.gradle.kts file to include the new module:


include(":app")
include(":baselineprofile")

Step 3: Create a Baseline Profile Generator

Create a new test class to generate the baseline profile. This test will interact with your app to cover the critical user journeys. This ensures those paths are optimized.


package com.example.baselineprofile

import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.ExperimentalStableBaselineProfileRule
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class BaselineProfileGenerator {
    @get:Rule
    val benchmarkRule = ExperimentalStableBaselineProfileRule()

    @Test
    fun generate() {
        benchmarkRule.collectStableBaselineProfile(
            packageName = "com.example.myapplication", // Replace with your app's package name
            profileBlock = {
                startActivityAndWait()

                // Example interactions - adapt to your app's user journeys
                val recyclerView = device.findObject(By.res("your_recycler_view_id")) // Replace with your actual ID
                recyclerView.fling(androidx.test.uiautomator.Direction.DOWN)
                device.wait(Until.hasObject(By.text("End of List")), 2000) // Wait for the fling to complete

                // Add more interactions as needed
            }
        )
    }
}

Update the above code:

  • Replace "com.example.myapplication" with your app’s package name.
  • Adapt the profileBlock lambda to cover important user interactions and code paths in your application. Use device.findObject to find UI elements and interact with them using methods like click and fling.

Step 4: Run the Baseline Profile Generator

Run the generate test on an Android device or emulator (API 31 or higher is recommended). This test will generate a baseline-prof.txt file in the src/androidTest/ directory of your :baselineprofile module.


./gradlew :baselineprofile:connectedCheck

Note: Make sure you’ve connected a device or emulator, or have one running.

Step 5: Move and Organize the Baseline Profile File

Move the generated baseline-prof.txt file from :baselineprofile/src/androidTest/ to src/main inside your main app module (e.g., :app).

Next, rename it to baseline-prof.txt and place it in the src/main/ directory of your application module (if the directory does not exist, create it).
Organize Profiles

When creating the baseline-prof.txt, the Gradle plugin will package the files in assets/dexopt/baseline.prof.

Step 6: Configure Release Build

Ensure that your release build is configured to take advantage of the Baseline Profiles. This is typically handled automatically, but it’s good to verify that no settings are disabling it.


android {
    buildTypes {
        release {
            // ... other configurations
            
            // This is usually enabled by default, but ensure it is not disabled
            // For Baseline Profiles to work correctly
            minifyEnabled true // Baseline profiles are meant for optimized builds
        }
    }
}

Step 7: Test the Performance Improvement

To measure the impact of Baseline Profiles, use the Macrobenchmark library to run startup benchmarks on your application with and without Baseline Profiles. Add this to your main app’s build.gradle file.


dependencies {
    androidTestImplementation("androidx.benchmark:benchmark-macro-junit4:1.2.2")
}

Create a simple benchmark test.


import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Test
import org.junit.runner.RunWith
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.junit4.MacrobenchmarkRule

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

    @Test
    fun startupCompiledNone() = startup(CompilationMode.None())

    @Test
    fun startupCompiledBaselineProfile() = startup(CompilationMode.Partial()) // or CompilationMode.Full() if appropriate


    private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
        packageName = "com.example.myapplication",  // Replace with your app's package name
        metrics = listOf(StartupTimingMetric()),
        compilationMode = compilationMode,
        startupMode = StartupMode.COLD,  // Or StartupMode.WARM, StartupMode.HOT as needed
        iterations = 10,  // Number of iterations
        setupBlock = {
            // Optional setup, like clearing app data
        }
    ) {
        startActivityAndWait()
        // Optionally add more interactions to cover different startup paths
    }
}

After implementing your performance benchmark and macrobenchmark tests. It can give feedback if Baseline Profiles are functioning as designed.

Step 8: Iterate and Refine

Performance optimization is an iterative process. Monitor user feedback, analyze crash reports, and regularly re-generate and update your Baseline Profiles to address new code paths or performance bottlenecks.

Conclusion

Baseline Profiles are a powerful tool for optimizing Jetpack Compose applications, leading to improved startup times, smoother performance, and a better user experience. By following these steps, you can effectively integrate Baseline Profiles into your development workflow and keep your application running at its best.