[MUSIC PLAYING]
RAHUL RAVIKUMAR: Good morning, everyone.
My name is Rahul, and I'm an engineer on the Android Toolkit
Team.
TOMÁS MLYNARIC: And hi.
I'm Tomás, and I'm engineer on the Developer Relations Team.
And, today, we're going to talk about how to make apps blazing
fast with baseline profiles.
RAHUL RAVIKUMAR: So far, we've announced baseline profiles
early this year, and we have about a quarter
million apps publishing with baseline profiles on the Play
Store.
This was even before some of the toolchain
for building and publishing with baseline profiles was ready.
Now that everything is ready, I'm going to make a claim.
You can probably make your app at least 30%
faster if you use baseline profiles.
The reason why we are so confident
will become clear once you understand
how code execution evolved in the Android platform.
We look at execution in three dimensions.
First is initial performance.
This is performance after an install
or after a process cold start.
Second is eventual app performance.
This is performance after the runtime
has a chance to discover some hot spots in your code,
for example.
And, third, we'll look at the cost of these optimizations
in terms of additional disk space required.
In Android 1, your code was interpreted,
so the runtime initial performance was slow.
Eventual performance was also slow, but, importantly,
we didn't use any additional disk space.
This is because we didn't really do much to optimize your code.
There's nothing to save.
This was a key consideration back then
because disk size in early devices
was really hard to come by.
In Android 2, Froyo, we introduced a just
in time compiler.
This additionally-- so even though your code started off
in interpreted mode, the just in time
compiler would discover hot parts and optimize them.
But those optimizations got thrown away
once the process died.
This is, again, a key consideration because disk
space was paramount.
In Android 5, we decided to do something slightly different.
We introduced full AOT, or Ahead Of Time compilation.
The way this work was after app install,
we ran a new compiler called dex2oat.
And this converted your dex code to optimize dex files.
So we compiled it at install time.
And once the user interacted with the app,
they only loaded the .odex files,
which is the compile code.
And, as a result, the user saw the performance improvements.
Now, two caveats here-- first, we introduced a new compiler
dex2oat.
Remember, this is running on a resource-constrained device,
so this is not particularly fast.
Also, we are optimizing the entire app.
Second, there's the cost of storing that compile code.
This also gave us this dialogue.
Anyone remember this?
[LAUGHTER]
The reason why this happened was every time
we introduced a new optimization,
we had to compile every single app on the device
to take advantage of those optimizations.
So this was clearly not ideal.
Going back, we improved some performance characteristics,
though.
So we made app launch faster because we have a new compiler.
We made disk size a bit worse because now
we have the cost of storing that compiled code.
So in Android 7, we decided to do
something that took advantage of the best of both worlds.
So we wanted to take-- we wanted to not compile the entire APK,
because that was slow, and we wanted to identify
the important parts of the app.
So to do that, we went back to a hybrid between just in time
and compiling the important parts of the code.
We called it profiler optimization.
And the way this worked was, you start off
the app in interpreted mode with the just in time
compiler, like you saw before.
But instead of throwing those results away,
we would store them, additionally,
in a file called Profile.
Now, Profile is just a list of classes and methods
that are important for us to convert to machine code
to make sure the app is fast.
Once the device was idle, that compiler
that you saw before ran in something we
call background dex2oat.
So we would only target the methods and classes
in the profile, and, now, we would only conditionally
compile them instead of compiling the entire app.
This was much faster because we knew exactly what
parts of the app are important.
And when the user was interacting with the app
the second time, we have the .odex and .dex files.
And, given we've optimized the important parts of the app,
the user sees the benefits in performance.
And not only that, this process repeats again and again.
So as just in time compiler discovers new hot spots,
it can dump them into the profile that
can be picked up by subsequent passes of background
dex2oat, which is great.
So let's look at the table, now.
So we've improved disk size usage
because now we're no longer compiling the entire app.
But now we have a mix of slow starts and fast starts.
We have slow start because your code starts off
in interpreted mode with the just in time compiler,
like before, and then eventually gets fast.
So if we were to plot app startup time and days
since install, you see something like this.
As the just in time compiler knows more, it optimizes more.
App launch typically gets better.
It's not just that.
The story repeats on every single user device.
It's more like this.
So every time your app is installed on a new device,
we start from scratch.
But wouldn't it be nice to take advantage
of profiles that are computed on devices
that are similar to yours but not your device--
so trying to crowdsource this profile information.
That's exactly what cloud profiles are.
So in Android 9, we introduce cloud profiles.
The idea is to identify common traits
in profiles on a device-- that are generated
on a device similar to yours and on a platform
version similar to yours.
And we start aggregating them.
So we identify those common traits,
and we upload that aggregated profile to Google Play.
Once we have enough of those profiles,
that profile is then provided to the app at install time
as APK metadata.
And then new users will see the benefits,
and they'll see a much flatter curve.
This is because we took advantage of profiles
from other devices, and then we used it on new devices.
So this is great.
One caveat, though-- the profile aggregation
can take several weeks because it really depends on
how often your app is used and how many users you have,
for example.
So the more users your app, the more
passes we have at background dex op Then
we can build a better profile.
Let's revisit that curve.
So now if you look at performance characteristics,
we have a new caveat here, which is
every time you release a new version,
we don't have profiles.
And so we have to start over again.
This happens every single time, every new version.
So if you've seen something like this before, we are sorry,
but now you know why.
[LAUGHTER]
OK, now, let's go back to the table.
We've improved on initial performance
because now we have Cloud profiles
from other devices supplanting profile information generated
on your device.
Let's look at the curve again.
This is the curve as it stands today during Android 9.
What we really want is this.
We want app startup to be fast in the beginning,
and we want it to remain fast.
That's where baseline profiles come in.
Baseline profiles are a way for the app
to provide what is important to the runtime.
So it's a way for you to bundle that information
into the app itself, and we consume that at publish time
and app launch time.
It's not just for apps, though.
It's also for libraries.
So when a library like Jetpack Compose
declares baseline profiles, all consumers reap the benefits,
and they can take advantage of it.
And, finally, I want to stress that the toolchain is now
stable.
So if you've seen problems before,
you won't run into them anymore.
I promise.
And it should be a much better experience.
Baseline profiles can be back ported all the way to Android 7
because we can take advantage of some of the same toolchain,
dex2oat, namely.
And we can backport a lot of the same functionality all
the way to Android 7.
That covers approximately 95% of the device population.
And so we solved the slow start problem
because you're telling us what to optimize ahead of time,
and then we give you great performance, to boot.
And if you include Android 5, which does full AOT,
that means profiles are used in 99% of the device population.
So that was a lot of theory.
And, now, I'll hand it off to Tomás,
who can show you how to generate them.
TOMÁS MLYNARIC: Thank you.
[APPLAUSE]
To get started with baseline profiles,
you can use Jetpack Macrobenchmark library
to generate them.
You write the simple instrumentation test--
let's call it baseline profile generator--
which will use a baseline profile rule.
That will provide us all the necessary scaffolding.
And we write a test method, generate,
in which we use collectBaselineProfile method
from the rule.
It requires two parameters-- first, the packageName
of the app you want to generate the profile for and,
second, the profileBlock lambda, in which you
write interactions of your app.
As part of the interactions, you should
cover your most important user journeys of your app,
such as app startup, scrolling the feed,
adding stuff to the basket of your e-commerce,
if you're e-commerce app, and stuff like that.
But, also, you may cover journeys
that are not called as often but are worth optimizing.
This could be a user login flow, sign-up flow, et cetera.
And that's because these journeys may not be covered
with cloud profiles soon.
But, also, if you've got some kind of A/B testing
in your application, you may consider
covering both of the scenarios because the slower performance
of one of the scenario actually can affect
the metrics of the A/B testing.
All right, let's write some interactions
for the Now in Android sample app.
We'll start with start activity and wait.
This will start the default activity of your application
and, actually, waits until the first frame is rendered.
Once the code of your app is executed,
the library takes care of capturing all the called
methods and classes in the background.
Next, we wait until the content is asynchronously loaded,
and ready, and shown onscreen.
And, again, once the test is executed,
the code will be collected again.
In our example, scrolling to feed
will be one of the most-used features.
So we find the feed on screen, and we scroll it
with the flick gesture.
Again, triggered code will be collected in a background.
So once the test is done, all the reference methods
and classes are saved on the device in a text file.
The file looks something like this.
There is no magic there, just some classes, methods
in a human and readable form.
You can read it, but you definitely
don't want to write it yourself.
So that's why we recommend generating it
with Macrobenchmark library.
And this file basically is the baseline profile.
So, next, you just need to copy the file
into src/main folder next to the unchecked manifest
and name it baselineprof.txt.
And that's it.
Next time, later, when you build a production version
of your app, if any dependency you use has a profile,
it will be merged with the one you just generated and included
into APK or app bundle in a compressed binary form
so that it doesn't take a lot of space.
And then you can just upload it to the Google Play
where it's processed for various device versions.
Once uploaded, it can be distributed to your users
in two ways.
The first and most optimized way is
when you install your application on Android devices
running 9 through Google Play.
That's where the cloud profiles also can be distributed.
So, in this case, when your users
install the app for the first time,
or they update it from the previous version,
it will download the APK and also
the process baseline profile.
And on device, the package installer installs the APK,
and it uses the included profile to run a targeted .dex
optimization at install time.
And that means that the optimized .dex files are
generated even before the app is launched.
And users see the performance benefits
from the first start of your app.
But also on Android 7, that's 7 and between 9,
the package installer doesn't know how to install the rules.
So we've got to file back for that.
And not only those versions, but also
if you don't ship your app through Google Play--
on other services-- that will work, as well.
The only thing you need to do is add androidx.profileinstaller
dependency to your app.
So, in this case, when users download the APK with baseline
profiles, the package installer installs the app,
but it doesn't optimize it immediately.
So when user opens the app after the install
or after the update, it runs in interpreted and just
in time mode, as before.
But, also, the profile installer knows
where the current profile--
the profile is and what format it needs.
And so we save the included baseline profile rules there,
similarly as the just in time compiler would over time.
And, later, when the device becomes idle,
the rules from the profile are compiled.
And we'll get the optimized text file
based on the baseline profile rules.
So next time the user opens the app,
it will be optimized and run fast.
This means you should always add the profile installer
to your dependencies.
Now, let's see how to verify the benefits of baseline profiles.
There are two ways.
The first option is, you can check your field metrics--
that is, Google Play Vitals, Firebase Performance
Monitoring, or any other service you
use for metrics of your app.
You should see improvements of your app startup
or smooth experience.
But that's only after you ship the version of your app.
To check it in the lab environment, like on a CI
or locally on a device, we can use the same Jetpack
Macrobenchmark library.
So, again, we write an instrumentation test.
And we use this time MacrobenchmarkRule
from the library.
This will provide scaffolding for benchmarks.
And, in this case, we write helper function, startup,
that we'll use in our tests later.
It will use measureRepeated method from the rule.
We need to fill in all the necessary parameters, that is,
how many iterations, what package to start,
and what metrics you care for in the benchmark.
So we care for the startup timing metric.
But then we'll also write one optional parameter
of the method, which is compilation mode.
This will help us distinguish between
when the app uses a baseline profile and when not.
And last thing, we write a measureBlock lambda
in which we specify the interactions with the app
similarly to the baseline profile generator.
So, here, we've got pressHome to start the app from the launcher
always.
We've got startActivityAndWait, and we
wait until the content is loaded and ready to use.
Now we've got the helper defined,
we just write two tests--
one that uses CompilationMode.None
that runs the app in the optimized state
without baseline profiles and one
that uses CompilationMode.Partial,
which partially precompiles the application.
We should run these benchmarks on a physical device.
If you can, run it on the most frequently represented device
so that you see the impact for the majority of your users.
The results come directly on Android Studio Output Pane.
You get duration in milliseconds.
So you get slowest, fastest, and median startup time.
So, in our case, what we can see is that the app started over
100 milliseconds faster with baseline profiles, which
is 40% improvements.
That's even long bigger than Rahul said in the beginning.
And the slowest run was even longer than that.
The other way to look at these results or verify if they work
is with system tracing.
Macrobenchmark records system tracing
of every iteration of the benchmark,
and it provides the traces within the same system--
within the same output pane.
So you can just click on the first iteration
of the CompilationMode.None, and it
will take you directly to the system tracing profiler.
So let's do it.
So, here, we've got the CompilationMode.None,
and we can see that in the system tracing profiler.
There, you could see a lot of information.
But what we want is we'll search for the jit thread pool.
That's the bottom pane.
We can see that during the startup,
there was a lot of work executing.
And that is the just in time compiler
compiling the interpreted code.
So that's the thing that is done doing.
If we open the other test-- the trace from the other test
from the CompilationMode.Partial,
we would see that in the jit thread pool,
there's almost nothing happening there.
And that's because the code is already compiled before the app
startup.
And that's the massive performance boost you can get.
You may see that there's still some tiny spec here
that can happen sometimes.
But with system tracing, you can investigate more,
and you can improve the profile generator, so that next time it
covers, actually it.
Now, performance is not just about numbers, and metrics,
and profiling.
It's actually about the actual user experience.
On the right side, we see that with baseline profiles,
the app starts faster.
But it's not just about app startup.
It's actually about smooth experience, as well.
So the apps grow smoother.
Animations run smoother.
So the performance gets better, in any case.
And, bear in mind that we didn't have to rewrite the app.
We didn't have to change the architecture of our app.
We just had to write an instrumentation test
and follow a simple process.
Your new users will get better first impression of your app,
and existing users will get consistent performance.
There are some links you can follow for more information.
We're here later to answer all the question.
Now, go, and make a baseline profile.
Thank you.
[APPLAUSE]
[MUSIC PLAYING]