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.
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
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
withCompilationMode.Full()
. Otherwise, it can be compared withCompilationMode.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:
- DeliveryHero tech blog on Baseline Profile is one of the best I found to know how it actually works internally. Interesting to read.
- About Baseline Profile
- Create Startup Profile
- Baseline Profile Codelab
- Make your apps efficient using BASELINE PROFILES (Loved It 🙂 )
This article is previously published on proandroiddev.com