Make your apps efficient using BASELINE PROFILES (Loved It :) )

Rohan jha
11 min readAug 18, 2024

--

Optimisation of your apps that helps enhancing user experience and eventually results in a greater business outcomes is very important. There are hell lot of realms of optimising your app and making it fast like using DSA concepts, Making reusable components, using interfaces for likewise event handling etc. And then there are some provided by Android itself in their official documentation. One of them is making use of basleine profiles. Lets dive deep into it : — )

Before understanding optimisation techniques and bechmark to be more specific. Lets first understand, what problem right now is and how baseline profiles solves that problem.

Problems :

Cold Delays : In starting, Android apps faced a slow start that actually reduces the experience of users. Reason behind the slow start was the compilation strategy of android i.e JIT (Just In Time). That basically means that all the dependencies used in the app, compiled just in time or at runtime leading the delays during the first launch of the app.

Longer Time To Smooth Interaction : UI interactions like scrolling and navigation shows the lag when accessed for the first time because dependencies are compiled on demand at runtime, so it takes some times to actually optimise the process and smooth the interaction.

Unopimised execution : JIT compilation sometimes perform partially optimised execution. Although JIT provides felxibility, but sometimes does not behave as efficient and fast as AOT (Ahead of time) compilation that eventually leads to suboptimal runtime performence. Click Here if you want to read more about AOT.

Battery drain and CPU load : When your JIT compilation keeps on working on demand then it actually affects your system as well and keep on consuming the battery and CPU usage uselessly.

Inconsistent user experiences across devices : On lower-end devices, where processing power and memory were limited, JIT compilation introduced more noticeable performance bottlenecks. This inconsistency in experience affected user retention and app ratings.

These were list of problems faced by users due to JIT compilation, there might be more but these are more recurred one. Now since we have seen the problems, Lets see what are the ways that baseline profiles actually uses to solve these problems and also the architecture that it works on.

Working of baseline profiles

Baseline profiles improves interaction time by 30% from the first start of the app by avoiding interpretation and JIT compilation steps for included code path. That means Android apps can optimize critical code paths even before the user starts interacting with the app for the first time. Here’s a breakdown:

1. Interpretation and JIT Compilation:

  • Normally, when an app is first launched, the Android runtime interprets the code or uses Just-In-Time (JIT) compilation to convert bytecode into native code. These processes are done on the fly, which takes extra time and resources.
  • During interpretation, code is executed line by line, which is slower.
  • JIT compilation compiles code just before it is needed, but this causes delays during the first execution of certain app interactions.

2. How Baseline Profiles Help:

  • Baseline profiles are precompiled profiles that define the most critical code paths, such as app startup routines and common UI interactions (like scrolling, navigation, etc.).
  • When a baseline profile is included in your app, it precompiles these code paths using Ahead-of-Time (AOT) compilation during installation. This means that when the user first launches the app, the code is already optimized and ready to run at native speed, avoiding interpretation and JIT compilation steps.

If you want to know about more benifits in real world scenario of AOT compilation or baseline profiles, then you can read it here.

How Baseline Profiles work

While developing your app or library, consider defining Baseline Profiles to cover common user interactions where rendering time or latency are important. Here’s how they work:

  1. Human-readable profile rules are generated for your app and compiled into binary form in the app. You can find them in assets/dexopt/baseline.prof. You can then upload the AAB to Google Play as usual.
  2. Google Play processes the profile and ships it directly to users along with the APK. During installation, ART(Android runtime) performs AOT compilation of the methods in the profile, resulting in those methods executing faster. If the profile contains methods used in app launch or during frame rendering, the user might experience faster launch times and reduced lag.
  3. This process works together with Cloud Profiles to gradually improve your app’s performance based on how people actually use it over time.
Source : Android documentation

Cloud profiles :

Cloud Profiles are a way to make your app run faster by optimizing how the code is compiled. Google Play gathers data on how real users interact with your app and creates these profiles. When people download or update your app, these profiles help it perform better right from the start.

However, there are a few limitations:

  • It can take several hours or even days for Cloud Profiles to be available after an app update, so users might not see the performance boost immediately.
  • Cloud Profiles only work on Android devices running version 9 (API level 28) or newer.
  • They work best for apps with a large user base, so smaller apps might not benefit as much.

In short, Cloud Profiles are useful for improving app performance, but they take time to be effective and have some device and app size limitations.

Since nearly everything is discussed and now you know the basic of baseline profiles, Lets see how we can actually implement it step by step so that you can solve the issue of your app JIT compilation issue with that .

Implementation

Automatically generate profiles for every app release using the Jetpack Macrobenchmark library and BaselineProfileRule. Recommend is that you use com.android.tools.build:gradle:8.0.0 or higher, which comes with build improvements when using Baseline Profiles.

Set up the Baseline Profile module

To run the Baseline Profile module template, follow these steps:

  1. Select File > New > New Module
  2. Select the Baseline Profile Generator template in the Templates panel and configure it:
Source : Android documentation
  1. The fields in the template are the following:
  • Target application: defines which app the Baseline Profile is generated for. When you have only a single app module in your project, there is only one item in this list.
  • Module name: the name you want for the Baseline Profile module being created.
  • Package name: the package name you want for the Baseline Profile module.
  • Language: whether you want the generated code to be Kotlin or Java.
  • Build configuration language: whether you want to use Kotlin Script (KTS) or Groovy for your build configuration scripts.
  • Use Gradle-managed device: whether you’re using Gradle-managed devices to test your app.

Click Finish and the new module is created. If you are using source control, you might be prompted to add the newly created module files to source control.

The Baseline Profile generator is a module that contains tests designed to create and measure Baseline Profiles. These profiles help improve app performance by optimizing key interactions, like app startup.

Key Points:

  • The module includes basic tests for app startup, but it’s recommended to enhance these with more complex workflows and key user journeys (CUJs).
  • When writing tests related to app startup, include them in a block where includeInStartupProfile is set to true. This ensures they’re part of the Startup Profile, which is a special section of the Baseline Profile focused on optimizing app launch.
  • For non-startup tests, make sure they are not included in the Startup Profile to avoid unnecessary optimization.

Best Practice:

  • Keep the code for CUJs separate from the Baseline Profile generation and benchmarking. This makes it easier to maintain, as any updates to your CUJs will automatically apply consistently across both tests and profiles.

Generate and install the Baseline Profile

The Baseline Profile module template adds a new run configuration to generate the Baseline Profile. If you use product flavors, Android Studio creates multiple run configurations so that you can generate separate Baseline Profiles for each flavor.

Source : Android documentation

When you run the Generate Baseline Profile configuration, it creates a profile and automatically copies it to the src/variant/generated/baselineProfiles/baseline-prof.txt file in the module you’re profiling. The "variant" here refers to either the release build type or a variant that includes the release build type.

Key Points:

  • The generated Baseline Profile is first created in the build/outputs folder.
  • The exact location depends on the variant, flavor, and whether you’re profiling using a Gradle-managed device or a connected physical device.
  • If you stick with the default naming conventions from the template, the profile will be created at:
    build/outputs/managed_device_android_test_additional_output/nonminifiedrelease/pixel6Api31/BaselineProfileGenerator_generate-baseline-prof.txt.
  • Normally, you don’t need to interact with this file directly. The system handles copying it to the right location automatically.

In short, once the configuration runs, the Baseline Profile is automatically moved to where it’s needed, and you generally don’t have to worry about the intermediate file paths.

Create a new Baseline Profile with AGP 8.1

If you aren’t able to use the Baseline Profile module template, use the Macrobenchmark module template and the Baseline Profile Gradle plugin to create a new Baseline Profile. We recommend you use these tools starting with Android Studio Giraffe and AGP 8.1.

Note: Automatic Baseline Profile generation with the Baseline Profile Gradle plugin is available starting with AGP 8.0, recommended is using AGP 8.1 for a better experience.

Here are the steps to create a new Baseline Profile using the Macrobenchmark module template and Baseline Profile Gradle plugin:

  1. Set up a Macrobenchmark module in your Gradle project.
  2. Define a new class called BaselineProfileGenerator:
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()

@Test
fun startup() = baselineProfileRule.collect(
packageName = "com.example.app",
profileBlock = {
startActivityAndWait()
}
)
  1. The generator can contain interactions with your app beyond app startup. This lets you optimize the runtime performance of your app, such as scrolling lists, running animations, and navigating within an Activity. See other examples of tests that use @BaselineProfileRule to improve critical user journeys.
  2. Add the Baseline Profile Gradle plugin (libs.plugins.androidx.baselineprofile). The plugin makes it easier to generate Baseline Profiles and maintain them in the future.
  3. To generate the Baseline Profile, run the :app:generateBaselineProfile or :app:generateVariantBaselineProfile Gradle tasks in the terminal.
  4. Run the generator as an instrumented test on a rooted physical device, emulator, or Gradle Managed Device. If you use a Gradle Managed Device, set aosp as the systemImageSource, because you need root access for the Baseline Profile generator.

Note: When using Jetpack Macrobenchmark 1.2.0-alpha06 and higher, you can generate the Baseline Profile on devices running Android 13 (API 33) and higher without root access.

At the end of the generation task, the Baseline Profile is copied to app/src/variant/generated/baselineProfiles.

Create a new Baseline Profile without templates

We recommend creating a Baseline Profile using the Android Studio Baseline Profile module template (preferred) or Macrobenchmark template, but you can also use the Baseline Profile Gradle plugin by itself. To read more about the Baseline Profile Gradle plugin, see Configure your Baseline Profile generation.

Note: The Baseline Profile Gradle plugin is already applied if you use the Baseline Profile module template.

Here’s how to create a Baseline Profile using the Baseline Profile Gradle plugin directly:

  1. Create a new com.android.test module—for example, :baseline-profile.
  2. Configure the build.gradle.kts file for :baseline-profile:
  3. Apply the androidx.baselineprofile plugin.
  4. Ensure the targetProjectPath points to the :app module.
  5. Optionally, add a Gradle-managed device (GMD). In the following example, it’s pixel6Api31. If not specified, the plugin uses a connected device, either emulated or physical.
  6. Apply the configuration you want, as shown in the following example.
plugins {
id("com.android.test")
id("androidx.baselineprofile")
}

android {
defaultConfig {
...
}

// Point to the app module, the module that you're generating the Baseline Profile for.
targetProjectPath = ":app"
// Configure a GMD (optional).
testOptions.managedDevices.devices {
pixel6Api31(com.android.build.api.dsl.ManagedVirtualDevice) {
device = "Pixel 6"
apiLevel = 31
systemImageSource = "aosp"
}
}
}

dependencies { ... }

// Baseline Profile Gradle plugin configuration. Everything is optional. This
// example uses the GMD added earlier and disables connected devices.
baselineProfile {
// Specifies the GMDs to run the tests on. The default is none.
managedDevices += "pixel6Api31"
// Enables using connected devices to generate profiles. The default is
// `true`. When using connected devices, they must be rooted or API 33 and
// higher.
useConnectedDevices = false
}

Create a Baseline Profile test in the :baseline-profile test module. The following example is a test that starts the app and waits for idle.

class BaselineProfileGenerator {

@get:Rule
val baselineRule = BaselineProfileRule()

@Test
fun startupBaselineProfile() {
baselineRule.collect("com.myapp") { // replace with your root package name
startActivityAndWait()
}
}
}

Update the build.gradle.kts file in the app module, for example :app.

  1. Apply the plugin androidx.baselineprofile.
  2. Add a baselineProfile dependency to the :baseline-profile module.
plugins {
id("com.android.application")
id("androidx.baselineprofile")
}

android {
// There are no changes to the `android` block.
...
}

dependencies {
...
// Add a `baselineProfile` dependency on the `:baseline-profile` module.
baselineProfile(project(":baseline-profile"))
}
  1. Generate the profile by running the :app:generateBaselineProfile or :app:generateVariantBaselineProfile Gradle tasks.
  2. At the end of the generation task, the Baseline Profile is copied to app/src/variant/generated/baselineProfiles.

Create a new Baseline Profile with AGP 7.3–7.4

It’s possible to generate Baseline Profiles with AGP 7.3–7.4, but we strongly recommend upgrading to at least AGP 8.1 so you can use the Baseline Profile Gradle plugin and its latest features.

If you need to create Baseline Profiles with AGP 7.3–7.4, the steps are the same as the steps for AGP 8.1, with the following exceptions:

Manually apply generated rules

The Baseline Profile generator creates a Human Readable Format (HRF) text file on the device and copies it to your host machine. To apply the generated profile to your code, follow these steps:

  1. Locate the HRF file in the build folder of the module you generate the profile in: [module]/build/outputs/managed_device_android_test_additional_output/[device].
  2. Profiles follow the [class name]-[test method name]-baseline-prof.txt naming pattern, which looks like this: BaselineProfileGenerator-startup-baseline-prof.txt.
  3. Copy the generated profile to src/main/ and rename the file to baseline-prof.txt.
  4. Note: If you’re using a version of the Android Gradle plugin earlier than 8.0, the baseline-prof.txt file isn't shown in the Android view in Android Studio.
  5. Add a dependency to the ProfileInstaller library in your app’s build.gradle.kts file to enable local Baseline Profile compilation where Cloud Profiles aren't available. This is the only way to sideload a Baseline Profile locally.
dependencies {
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
}

Build the production version of your app while the applied HRF rules are compiled into binary form and included in the APK or AAB. Then distribute your app as usual.

Best Practices and Things to Keep in Mind

  1. Target Key Code Paths: Focus on optimizing the most frequently used code paths like startup, main screens, and user interactions.
  2. Use Realistic Scenarios: The interactions in your baseline profile tests should mimic actual user behavior to be effective.
  3. Keep the Profile Updated: As your app changes over time, regularly update the baseline profiles to reflect new flows or optimizations.
  4. Profile for Different Devices: Consider testing on a range of devices to ensure the profile works well across different hardware.
  5. ProfileInstaller Integration: Always include the ProfileInstaller library to make sure the profile is used when the app is installed from the Play Store.
  6. CI Integration: Automate the generation and validation of baseline profiles in your CI pipeline to keep profiles fresh with every release.
  7. Monitor Performance: Regularly monitor app performance, even after implementing baseline profiles, to catch regressions early.

With this we have seen every aspect of baseline profile usage for optimisation of your app. Hope this helped. See you in the next one.

--

--

Rohan jha

Android developer. Java Backend developer. Exploring Cloud