Improve Your Android App Performance With Baseline Profiles

Application performance holds paramount significance as it directly correlates with the overall user experience. Within the developer community, numerous methods exist for enhancing application performance. In this post, you will delve into one of these techniques by utilizing the Baseline Profile.

Jaewoong E.
Jaewoong E.
Published December 6, 2023

Baseline Profiles empower you to achieve faster code execution, boasting potential speed improvements of around 20-30% from the first launch. This is accomplished by delivering pre-compiled source code information, effectively bypassing interpretation and just-in-time (JIT) compilation steps when users launch your application.

When you integrate Baseline Profiles into your application, Android Runtime (ART) optimizes precise code pathways by utilizing the supplied source code profiles. These profiles consist of information regarding your classes and methods employed in Ahead-of-Time (AOT) compilation.

For library authors, Baseline Profiles present an excellent choice for delivering optimal developer experiences. Baseline profiles can also be packaged within libraries and subsequently incorporated into user applications, where the library's code pathways are utilized to enhance users' app performance.

Ideal Scenarios to Integrate Baseline Profile

In most scenarios, the Baseline Profile enhances your app's performance in the manner described earlier. However, this improvement is particularly pronounced when your project relies on numerous third-party libraries, including popular Jetpack libraries. In such cases, your app experiences even more substantial optimization and performance enhancements.

For instance, if your app relies on a multitude of Jetpack Compose libraries, you might encounter some minor jankiness or lag during the initial launch of your application. This is an anticipated outcome, as Compose operates as a library, so it doesn't engage in system resource sharing and the Android platform. Therefore, ART should interpret these libraries heavily when the app launches and run JIT interpretation when the functionality is needed, decreasing your app's performance and causing it to be slow on startup.

While certain Jetpack libraries already contain Baseline Profiles, note that most Jetpack libraries do not have them. Moreover, many libraries from GitHub or developer communities also don't contain Baseline Profiles.

Considering the specifics of your project, it would be great to integrate Baseline Profiles into your project rather than relying on libraries to supply them if you expect to optimize your app performance.

Set Up Baseline Profile Module

Now, let's proceed with configuring the Baseline Profile for your project. Starting from AGP 8.0 or higher, you have the convenience of utilizing the Baseline Profile Gradle plugin. This plugin streamlines the creation of Baseline Profiles, provides package filtering options, and offers more convenient features, including flavor control.

If you're using Android Studio Iguana (or a more recent version) along with Android Gradle Plugin 8.2, you can effortlessly create a Baseline Profile module by making use of a template offered by Android Studio.

When you navigate through the steps: File -> New -> New Module within your Android Studio, you will see the Baseline Profile Generator template, illustrated in the image below:

Ensure that you've identified the specific project or module you wish to target for Baseline Profiles generation, and proceed to configure the package name for the Baseline Profile module accordingly. Upon clicking the Finish button, you'll notice the creation of a module that houses the BaselineProfileGenerator.kt file.

If your project encompasses multiple variants, and you aim to govern behaviors like merging the generated profiles for all variants into a unified profile or automating Baseline Profile generation during project builds or release assembly, refer to the Configuring Baseline Profile generation guide for comprehensive instructions.

Baseline Profiles For Libraries

For library authors, including Baseline Profiles within your library can enhance the developer experience. To set up Baseline Profiles for your library, simply add the Baseline Profile plugin as you would configure it for the target module.

kt
1
2
3
4
plugins { id("com.android.library") id("androidx.baselineprofile") }

Following that, include the Baseline Profile dependency that you established in the previous section.

kt
1
2
3
4
5
dependencies { ... // Add a `baselineProfile` dependency on the `:baseline-profile` module. baselineProfile(project(":baseline-profile")) }

Finally, you must set package filters, as demonstrated below, to prevent the inclusion of all unrelated classes and method information in the generated Baseline Profiles:

kt
1
2
3
4
5
6
7
8
baselineProfile { // Filters the generated profile rules. // This example keeps the classes in the `com.library` package all its subpackages. filter { include "com.mylibrary.**" } }

Generate Baseline Profiles

You can now directly generate Baseline Profiles from the Run dialog in Android Studio, just as illustrated in the image below:

In most cases, you will be able to generate Baseline profiles using the basic scenarios provided in the BaselineProfileGenerator.kt file. This can be achieved by either selecting the menu option above or by entering the command line below into your terminal:

bash
1
./gradlew generateBaselineProfile

If the execution is successful, you'll discover the generated Baseline Profiles named baseline-prof.txt within each module, residing under the /src/main/generated/baselineProfiles directory. When you click on the text file, you'll find the class and method declarations, presented as shown below:

As evident in the preceding screenshot, the Baseline Profiles encompass class and method information pertaining to your application or library. This information is utilized to optimize execution for efficiency within the Android runtime environment.

If you encounter issues generating Baseline Profiles for any reason, consider the following troubleshooting steps:

  1. Double-check that your baselineprofiles module has the correct targetProjectPath and that your target project module has successfully configured the Baseline Profiles plugin.

  2. If your project is configured with multiple flavors or version name suffixes, ensure that the package name used in both the baselineprofiles module and the BaselineProfileGenerator.kt file is appropriate for your project's specific flavor or version.

Analyze APK and AAB Files

Once you’ve generated Baseline Profiles, let's examine the APK/AAB files. Now, you can verify if the Baseline Profiles have been correctly integrated into your APK/AAB files and are ready to be delivered to users.

Once you create an APK/AAB file of your project, you can open it via Build -> Analyze APK menu on Android Studio. If you open your APK/AAB file, you’ll find the obfuscated Baseline Profile in under assets/dexopt/baseline.prof directory for APK or BUNDLE-METADATA/com.android.tools.build.profiles/baseline.prof for AAB.

The baseline.prof file is generated from the baseline-prof.txt you provided by obfuscating into the dex file when you package your project into an APK/AAR result. But how exactly does this baseline.prof file enhance your app's performance? The magic key lies in the DEX optimizer, known as Dexopt.

If your APK/AAR file contains the baseline.prof file, the Dexopt will follow the three primary processes below to improve app performance.

  • Dex to OAT Compilation: Initially, the .dex files are converted to a more efficient format called OAT (Optimized Android Application Package).

  • Ahead-of-Time (AOT) Compilation: During this step, the OAT files are further optimized by compiling them ahead of time. This process includes optimizing code for the specific CPU architecture of the device on which you install the app. It reduces the time it takes to launch an app and can improve overall runtime performance.

  • Caching: The optimized OAT files are cached, so they can be quickly accessed on subsequent launches of the app, reducing the need for repeated optimization.

Therefore, the DEX optimizer leverages your Baseline Profiles to enhance your app's startup time and runtime performance, following the steps outlined above.

Analyze AAR Files

If you're a library author, you can verify whether your AAR file accurately incorporates the Baseline Profiles. Compile your project and, using Android Studio, open your AAR file by following the same steps as detailed in the preceding section; then you’ll see the result below:

As evident in the screenshot above, you'll observe that the baseline-prof.txt file is securely embedded within your AAR file. At this point, you might wonder, "Why does it carry a .txt extension, unlike the APK/AAB files?"

The answer to this question can be found in the IssueTracker: Baseline Profile is not obfuscated for AAR library. According to the Baseline Profile team's response, "Libraries are not typically obfuscated, and AGP currently does not disambiguate between debug vs. release versions of a library." So, if you've noticed a .txt file instead of a .prof file, it's entirely as expected.

1.5 MB Limitations

The official Android documentation states that "Compiled Baseline Profiles must be smaller than 1.5MB." Let's explore the rationale behind this limitation.

Navigate to the directory where the generated baseline-prof.txt file is stored, and inspect the file's properties. You'll likely notice that the file size exceeds the 1.5 MB limit, often ranging from 4 to 10 MB or higher.

Then can’t we contain our Baseline Profiles inside our APK/AAB? The answer is nope. The official Android documentation said, “The compiled binary Baseline Profiles,” not the text file. Then let’s see the size of the compiled binary Baseline Profiles.

To investigate this, open your APK/AAB file using Android Studio's Analyze APK menu, and access the assets/dexopt/ directory. Inside, you'll encounter the baseline.prof file. Upon checking its properties, you'll notice that the file size typically falls within the range of 10 to 30 KB or even higher.

Indeed, this size is significantly smaller than 1.5 MB, right? When you package your Baseline Profiles into an APK/AAB file, a substantial portion of the profile information is optimized, considerably reducing the compiled size. In the majority of cases, you need not be overly concerned about the specified limitations.

Improve Benchmark Scenarios

Now that you've delved into generating Baseline Profiles and comprehended their integration into your app, there's one more crucial aspect to supercharge your startup and runtime performance – crafting effective benchmark scenarios.

If you've relied on the default scenario provided in the BaselineProfileGenerator.kt file, it's essential to note that this scenario only captures your app's initial launcher activity without accounting for additional user experiences and behaviors. Consequently, your Baseline Profiles might lack the comprehensive information needed to encompass the broader range of scenarios that users might encounter.

To tailor your Baseline Profiles to encompass a wider array of user interactions, you should craft your own app scenarios using Macrobenchmark and UIAutomator libraries. You can create automated UI tests, which allow you to simulate various user actions, including navigating to different screens, clicking buttons, scrolling, interacting with UI components, and even emulating physical device button presses.

Based on your automated UI test, Macrobenchmark extends its profiling and recording to produce Baseline Profiles with a broader scope of your application, capturing a more extensive range of profile information. If you're interested in learning how to create efficient benchmark scenarios, refer to the GitHub repositories below:

Measure Startup Performance

Now, let's gauge the real-world impact of Baseline Profiles on your app's startup and runtime performance. You can easily measure your app's startup performance by crafting benchmarking code using Macrobenchmark, as demonstrated in the following example:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class StartupBenchmarks { @get:Rule val rule = MacrobenchmarkRule() @Test fun startupCompilationNone() = benchmark(CompilationMode.None()) @Test fun startupCompilationBaselineProfiles() = benchmark(CompilationMode.Partial(BaselineProfileMode.Require)) private fun benchmark(compilationMode: CompilationMode) { rule.measureRepeated( packageName = "YOUR_APPLICATION_PACKAGE_NAME", metrics = listOf(StartupTimingMetric()), compilationMode = compilationMode, startupMode = StartupMode.COLD, iterations = 10, setupBlock = { pressHome() }, measureBlock = { startActivityAndWait() device.waitForIdle() // app sceniarios }, ) } }

Ensure that you've employed the correct package name and that your app scenarios align with those utilized in the BaselineProfileGenerator.kt file. In the provided code snippet, you can observe that you're benchmarking two scenarios for your app's performance: one without Baseline Profiles and the other with Baseline Profiles.

After executing your performance measurement test, you'll find the report presented in Android Studio as follows:

StartupBenchmarks_startupCompilationBaselineProfiles
timeToInitialDisplayMs   min 341.7,   median 419.6,   max 765.2
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

StartupBenchmarks_startupCompilationNone
timeToInitialDisplayMs   min 371.4,   median 434.7,   max 815.9
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

The report indicates that Baseline Profiles contribute to an increase in app performance, ranging from 7% to 10% in the min/max cases.

If the measurements indicate superior performance in the non-Baseline Profiles scenario, it's essential to scrutinize whether your app scenarios heavily rely on diverse features, contingent upon various situations, such as numerous network or database requests.

Generating Baseline Profiles With GitHub Actions

Typically, you can generate Baseline Profiles whenever you create a release build by utilizing the plugin option below:

kt
1
2
3
4
5
6
7
baselineProfile { variants { freeRelease { automaticGenerationDuringBuild = true } } }

You also have the flexibility to generate Baseline Profiles tailored to more specific scenarios based on your preferences through GitHub Actions as you can see in the script below:

yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
name: baseline-profiles run-name: ${{ github.actor }} requested a workflow # This should be a manual trigger so this actions gets executed every time make a new pull request. # Change this event to what suits your project best. # Read more at https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows on: workflow_dispatch: jobs: generate-baseline-profiles: runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: distribution: temurin java-version: 17 - name: Setup Gradle uses: gradle/gradle-build-action@v2.9.0 - name: Grant Permissions to gradlew run: chmod +x gradlew - name: Build app and benchmark run: ./gradlew :app:assembleBenchmark # change the 'app' with your app module's name - name: Clean Managed Devices run: ./gradlew cleanManagedDevices --unused-only # Generates Baseline Profile - name: Generate Baseline Profile run: ./gradlew generateBaselineProfile -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile -Pandroid.experimental.testOptions.managedDevices.setupTimeoutMinutes=20 -Dorg.gradle.workers.max=4 # Create Pull Request - name: Create Pull Request uses: peter-evans/create-pull-request@v5 with: commit-message: "Generate baseline profiles" title: "Generate baseline profiles" delete-branch: true branch: actions/baseline-profiles

As the workflow runs, it will automatically initiate a Pull Request, updating Baseline Profiles as depicted in the image below:

Conclusion

Throughout this article, you've delved into a comprehensive understanding of Baseline Profiles. You've explored how Baseline Profiles work beneath the surface with the Dex optimizer, learned to analyze your APK/AAB/AAR files, discovered how to create benchmark scenarios, and how to measure your app's performance.

If you want to learn more about Baseline Profiles and Benchmark, check out Baseline Profiles Overview and What’s new in Jetpack Benchmark 1.2.0.

You can find the author of this article on Twitter @github_skydoves or GitHub if you have any questions or feedback. If you’d like to stay up to date with Stream, follow us on Twitter @getstream_io for more great technical content.

As always, happy coding!

Jaewoong