Shipping precompiled apps reduces Android boot time on Raspberry Pi

Android-icon

The first boot of a fresh Android image is always the slowest one. Before the launcher appears, the system tries to optimize every installed app on a freshly formatted userdata partition. We moved that optimization into the build itself, so the apps ship already optimized, and measured what it saved on Raspberry Pi 5: 32% off the optimization phase on first boot, and 50% off the same phase on every boot after that.

This is the second post in our boot performance series. The first article covered a kernel flag that was capping SD card throughput. This one is a different lever further, along the same boot timeline.

Where the time actually goes

Android logs timestamped milestones during startup process. Two of them bracket the work we're looking at here: package-manager-ready and activity-manager-ready. On a Raspberry Pi 5 with a freshly formatted data partition, almost nothing under the apps directory has been compiled yet, so the runtime spends this window optimizing app after app before the home screen can come up.

Measuring the gap between those two milestones directly isolates this phase cleanly from everything else happening during boot. On a fresh image it took 24.5 seconds, which is too long for professional devices.

Compiling ahead of time instead of on the device

Android's runtime supports several optimizations profiles at compile time, which control if logic is executed at runtime on the device, or just once during the build. Among other possible profiles, these two seemed the most relevant in our case for booting Android on Raspberry Pi:

  • The default profile for many bundled apps is "verify". With it, the bytecode gets checked for safety, then run through the interpreter and the just-in-time compiler runs at runtime on the device.

  • Another profile "speed" optimizes the app at compile time and creates native code for it right away. This avoid additional runtime overhead at runtime.

We raised the bundled system apps profile from verify to speed, so their compiled pre-optimized artifacts are part of the Android OS image itself:

# status of a bundled app on fresh image before and after our change
- com.android.settings   status=verify   reason=prebuilt
+ com.android.settings   status=speed    reason=prebuilt

Effect it had on device's boot time

We measured the optimization phase on the same hardware before and after the change,. We used a fresh image to record the time for the very first OS boot and measured it again after a normal reboot to check subsequent boots.

The table below shows the measured time as the interval between the package-manager-ready and activity-manager-ready boot events on a Raspberry Pi 5, Android 16, 4 GiB RAM, SD card storage.

  Before (verify) After (speed) Change
Fresh image, first boot 24.5 s 16.7 s -32%
Every later boot 4.0 s 2.0 s -50%

The two numbers come from two different costs:

  • The 7.8 seconds saved on first boot is a one-time compilation cost. A fresh image is the only time the device has to compile every installed app from scratch and write the result to the data partition. Shipping that native code in the image removes the step outright, and because the result persists on disk, that compilation never runs again on any later boot.

  • The two seconds saved on every later boot is a recurring cost instead. Before the home screen appears on any boot, the system has to start its boot-critical software, the system UI, the launcher, and the core services behind them. Left at the baseline compilation level, that code has no native compilation, so it runs through the interpreter and warms up under the just-in-time compiler every single time it starts. At the speed level, it runs as native code from the first instruction. Precompiling removes that warm-up on every boot, but it only applies to the handful of apps actually on the boot path rather than the whole catalog, which is why the recurring saving is two seconds while the one-time saving is eight.

What it did not change

We also checked whether ahead-of-time compilation will make apps open faster too, but in our case, it did not help. We launched several apps cold, five times each, on the same device and boot state, changing only the compiler level, and measured time to first frame.

Following table shows the median of five cold launches per app, same device and boot state, only the compiler level changed. The differences are within run-to-run variation.

Cold start to first frame Verify Speed Speed-profile
Settings 391 ms 394 ms 384 ms
Cromite 347 ms 336 ms 336 ms
Calendar 246 ms 245 ms 246 ms

App launch to first frame is dominated by forking the process, loading classes, and laying out the first screen. Ahead-of-time compilation speeds up sustained code execution, which is not what this path spends its time on, so the compiler level an app was built with does not move the number. Precompiling pays off at boot, not at launch, because boot is where a large batch of code executes just once and needs to be fast in aggregate, while a single app launch depends on other costs entirely.

Future optimization work

We are continuing this investigation on different platforms we support. In future posts in this series we want to apply these findings to an NXP-based build, and show why it landed even better there.

If you're building commercial products based on Android and Raspberry Pi, and the boot time matters for your use case, this optimization was already included into the latest Emteria image for Raspberry Pi 4 and Raspberry Pi 5.

Build unique products, boost device performance

See why emteria is the chosen Android™ customization & management platform for OEM solution builders — and what it can do for your team and customers.

Book live demo
Android-head-use-cases

Table of contents