Generating Baseline Profiles for Authenticated Flows
What Baseline Profiles are
Baseline Profiles are one of the easiest performance wins on Android. The idea is simple: you record which classes and methods your app uses on its key screens, ship that list inside the APK, and ART (Android Runtime) pre-compiles those classes at install time. The result is a faster cold start — because without this, Android falls back to the just-in-time (JIT) compiler, which takes its toll on first runs.
Modern Android versions do eventually compile the hot paths of frequently used apps on their own — after a few sessions, the second and third opens get faster. A Baseline Profile means you deliver that optimal performance on the very first open instead of making users warm the app up themselves.
The standard way to generate one is the Baseline Profile Gradle plugin: you write a Macrobenchmark test, it opens your app on a device via UI Automator, and the collected profile is written to baseline-prof.txt.
The problem
The banking app we're working on has this set up too. I ran the gradle command and watched the connected device:
./gradlew :app:generateReleaseBaselineProfile
The app opened, showed the onboarding screen, and closed. That was the whole run.
Once you think about it, it makes sense: the generator launches the app in a clean, logged-out state, and in a clean state the only reachable screens are onboarding and login. So that's exactly what our profile covered — and nothing more. The home screen, money transfers, payments, card management — the screens where users actually spend their time — weren't in the profile at all.
Why I didn't automate the login
The first idea was the obvious one: automate the login inside the Macrobenchmark test. But for a banking app, every variant of that idea has a problem:
- Real test credentials — a working bank login sits in the repo and runs in automation. That's wrong from a security standpoint, and on top of that our login includes a Face ID step, which is hard to automate anyway.
- A login backdoor for tests — also a security mistake; adding a bypass to an app whose whole job is not having bypasses.
- A fake login behind a separate build flavor — here we could pretend to "log in" and walk the main screens. But the core problem is that it requires significant changes to the app's code, which then has to be maintained forever.
In all three cases, the app's code has to change to satisfy the tooling. It should be the other way around — the tooling should adapt to the app.
The insight
After some research, everything became clear: the Gradle plugin and UI Automator are just a convenience layer. The real source of profile data is ART itself. The runtime continuously records which classes and methods run, regardless of who is driving the app — a real human or UI Automator, it makes no difference.
And as a real human, we can get past the login, exercise all the important parts of the app, and the baseline profile is built from our actions along the way.
So the plan became: set the automation aside, walk through the app by hand, and pull ART's profile directly over adb.
The workflow
You'll need an Android 14+ (API 34+) device for the profile dump commands.
# 1. Build the profileable but non-debuggable variant
./gradlew :app:assembleNonMinifiedRelease
# 2. Install it
adb install app/build/outputs/apk/nonMinifiedRelease/app-nonMinifiedRelease.apk
# 3. Clear old profile data to start from a clean slate
adb shell pm art clear-app-profiles com.example.bank
# 4. Use the app like a regular user:
# log in, pass biometrics, walk through all the flows you need covered
# 5. Dump the recorded profile and pull it to your computer
adb shell cmd package dump-profiles --dump-classes-and-methods com.example.bank
adb pull /data/misc/profman/com.example.bank-primary.prof.txt
# 6. Put the file in place of baseline-prof.txt and commit
cp com.example.bank-primary.prof.txt app/src/release/generated/baselineProfiles/baseline-prof.txt
One important detail: we still run the standard Gradle task, because it generates a perfectly good startup-prof.txt — the logged-out startup path is the same for every user. We just don't use the baseline-prof.txt it generates, since that file only knows the pre-login world. We keep the hand-collected profile instead.
Keeping profiles fresh: Fastlane
The main problem with manually generated profiles is the human factor. A few releases later, or when developers on the team change, this whole approach can simply be forgotten.
So I built it into the release process. We use Fastlane for releases, and before every release command runs, we ask the person doing the release to confirm the baseline profiles are up to date:
up_to_date = UI.confirm("Did you make sure baseline profiles are up to date?")
unless up_to_date
UI.user_error!("Update the baseline profiles, then re-run this lane.")
end
A simple solution, but updating the profiles is now a step the release cannot skip. And nobody has to memorize the adb commands — the process itself shows you what to run.
Results and drawbacks
Coverage went from ~50K to ~107K lines — more than double. The profile now includes login and the main post-authentication screens, not just onboarding.
Of course, this approach has its drawbacks:
- The process is manual, so it can't be automated in CI — profile quality depends on a person walking the right screens.
- If you miss a screen during the walkthrough, that screen doesn't make it into the profile.
- Without a check like the Fastlane one, the profile silently goes stale as the app evolves.
If your app's login can be safely automated — a test environment, throwaway credentials, no biometric wall — the Macrobenchmark route is still the better solution.
Takeaway
UI Automator is just a convenience. ART collects the profile regardless of who drives the app. If Automator can't get past your login screen, you can go one layer down and take the data directly.
If your Baseline Profiles look suspiciously small and your app has a login screen — check what your generator is actually seeing. It might be profiling nothing but your login screen.