[MUSIC PLAYING]
SHUKANG ZHOU: Hello everyone.
My name is Shukang.
I'm a software engineer from the Android Studio team.
YI YANG: Hi, my name's Yi.
I'm also a software engineer from the Android Studio team.
SHUKANG ZHOU: So today we are going
to talk about Profilers in Android Studio.
As you may have known, our Profilers
provide the CPU, memory, network,
and energy usage information for developers
so they can optimize their app to be leaner and faster.
Today, I'm going to be focused on Memory Profiler.
A lot of developers come to Memory Profiler
because they are looking for a memory leak.
So what is a memory leak?
In the conventional definition, a memory leak is a object.
It becomes inaccessible to the running program,
but still occupies the memory.
If you have ever worked with pointers in C or C++,
you may be familiar with this concept.
But in the world of Kotlin and Java,
things are a little bit different
because their programs are running in Java Virtual
Machines.
And in JVM, there is an important concept
called garbage collection.
When garbage collection happens, the virtual machine
will first identify Garbage Collection roots.
A GC root is an object that is accessible
from outside the heap.
Examples include local variables, and the running
thread, and et cetera.
Then the virtual machine will identify
all the objects that are accessible from these GC roots.
They will be retained.
For the rest of the objects that cannot be accessible from any
of these GC roots, they will be considered garbage and will be
recycled.
So therefore, the conventional definition of memory leak
does not exist in JVM.
So what we mean when we say memory leak in JVM?
We usually mean abandoned objects.
The program is never going to use it again,
but it's still accessible.
So in Android, there are two types
of objects that you really don't want to leak them--
fragments and activities.
Because they often consume a lot of memory.
We are happy to say that from Android Studio 3.6,
Memory Profiler can automatically
detect leaks, activities, and fragments.
In order to do that, the first thing you do to capture heap
dump by pressing this button in the Memory Profiler.
Then after the heap dump is loaded,
you select this checkbox.
Then the provider will show possibly leaked activities
and fragments.
So how can the Profiler tell that?
Because these two classes have real defined behavior.
So for activity-- if it has been destroyed--
then we know the program is not going to use it again.
So if it's still referenced, it's a leak.
So for fragments, a fragment has to be
associated with a fragment manager to be used.
So if we don't see a fragment manager is with the fragments,
and if it's still referenced, it's a leak.
But there is a caveat.
If you get your heap dump at the interesting point
after the fragment is created but before it's getting used,
the Memory Profiler will report a false positive.
And so it's true if the fragment is cached but not yet reused.
So I have talked about activity in fragments.
So how about other projects or other objects?
Next, I'm going over the data in Memory Profiler
that are designed to help you identify the leaks.
So the first thing after you get a heap dump,
you will see a list of classes.
So for each of the classes, we show the number
of instances, which is the allocations
column in the table.
Next we show native size, shallow size,
and retained size.
These data are important because they let you pinpoint
where the memory is consumed.
Next, I'm going to use this example
to expand exactly what they mean.
I want you to pay attention to this red note.
In this example, it also referenced to our object
in the native heap.
This is not very common, but it does happen.
For example, after Android 8.0, the bitmap
will save the pixel data in the native heap
to reduce the memory pressure on the JVM.
So let's start with the shallow size.
It's very simple.
It's the memory consumed by the object itself.
Native size is also simple.
It's the size of the native objects referenced
by the red note.
Retained size is interesting.
It's the memory consumed by all of these orange notes.
Why?
Because if you delete the red note, all of these orange notes
will also be deleted because they become inaccessible.
So in a sense, they are retained by this red note.
That's why it's called retained size.
Note this object to the right.
It's not included in the retained size
because it's still accessible even if the red note is gone.
So by using this data, you can identify the classes
that are worth attention.
You click on the class name, the memory provider
will open a instance view showing a list of instances
of this class.
There is new kind of data here-- depth.
The depth is the length of the shortest path from the GC
root to this instance.
So the numbers on the graph are the depth of these objects.
Why do we show them?
Because the closer an object is to the GC roots,
it has a higher chance that multiple paths
exists from the GC root to this object.
And as a result, it has a lower chance to be garbage collected.
So take the red note as example.
If any of the reference to the left is broken,
the red note will become inaccessible
and the garbage collected.
For the blue notes on the right--
if you want to make it a garbage,
you have to break the path from the left and the path
from the right.
If you see any instance has a depth of 1,
that's a very strong signal.
Because it means that instance is directly
referenced by some GC root.
Here I'm showing a code example.
Here we have activity.
It implements LocationListener interface.
And the highlighted method--
requestLocationUpdates-- will register this activity
to locationManager.
If you forget to unregister, this activity
will become a leak.
It will stay in the heap forever because location manager
is a GC root and will stay there forever.
You can verify this in the Memory Profiler.
If you click an instance in the instance view,
Memory Profiler will open a reference panel.
And here we can show who is referencing to that instance.
And here we can see the mListener
field in location manager is referencing this activity.
And actually, you can navigate the reference graph
of the heap using the reference panel so
that you can verify if those reference chains is expected.
That can help you to understand if there's a leak
and where is the leak.
So I have talked enough about the Memory Profiler.
Now, let us please switch to the demo.
So here I have a Android Studio opening
a sample app called Displaying Bitmap.
It's a very simple app, not a real one,
but it serves the purpose for today's talk.
The first thing I do is I click this profile button
to deploy this app and start profiling it.
So this app has two activities.
The first activity shows a grade of images.
If I click one, we go to the second activity
showing the form image.
Now I return to the first activity.
At this point, the second activity
should be garbage recycled.
And we can examine if that's the case in the Memory Profiler.
I go to the Memory Profiler.
I get a heap dump.
Now I want to make a disclosure.
I didn't find any memory leaks in this simple app,
so I added one to show you what a leak looks
like in the Profiler.
So now I select this checkbox.
And we can see the Memory Profiler reports image detail
activity is a leak.
So I click the class name, I see the instance.
I click the instance, I see the reference.
And we can see it's the locationManager
is referencing this activity, just
like we discussed a moment ago.
So this is what we are going to tell you about Memory Profiler
today.
Next, I'm going to hand over the rest of talk to Yi.
[APPLAUSE]
YI YANG: Thank you, Shukang.
Now let's switch gears to the CPU Profiler.
Just like the Memory Profiler, the CPU Profiler
also provides a way of capturing critical performance data
to help you profile your app.
The feature is called CPU Recording.
To capture a recording, navigate to the CPU Profiler
and then press this Record button.
Perform some action in your app, come back to the Profiler,
and press the Stop button.
So now we have a recorded trace here.
The first thing I want to point out is this thread list here.
Because a recorded trace is organized by threads,
you want to make sure you are selecting
the correct thread before looking into the trace data.
On the bottom here, we have four tabs showing you
four different charts--
Call Chart, Flame Chart, Top Down, and Bottom Up.
The Call Chart may be straightforward,
but what is a Flame Chart?
Why does it have a shape like this?
And what about Top Down and Bottom Up?
What do these numbers mean?
Well, let's dive right in.
Can we switch back to the site?
So CPU Recording is actually just a collection
of call stacks.
In the case of System Trace, it's
a collection of event stacks.
But they behave the same.
So for the rest of the talk, I'm just going to use call stacks.
So a recorded trace contains a tremendous amount of data--
even for a short trace-- and is not human-readable.
That's why we need a few ways to visualize them.
Let's start with the Call Chart.
A Call Chart is probably the most straightforward one.
It's basically a visual representation
of the call stacks.
Think of this as a reconstruction of the program's
running call stacks.
The horizontal axis is just the timeline
showing you exactly when the method starts and when it ends.
The vertical axis shows callers on the top
and callees on the bottom.
So the Call Chart is much easier to parse than raw data already.
But it's not very easy to discover
parts of the code that's long-running.
That's why we need a Flame Chart.
So the Flame Chart provides an aggregated view
of the call stacks.
Unlike the Call Chart, the horizontal axis
here shows duration percentage.
So by discarding the timeline information,
we can now aggregate the data and show
the percentage each call takes relative
to the entire recording.
And also the vertical axis is inverted,
showing callees on the top and callers on the bottom.
So the name Flame Chart comes from the shape.
The higher you go, the narrower it gets--
just like a burning flame.
But how exactly do we aggregate the call stacks?
Well, we start with a Call Chart,
merge identical calls stacks, sort them by duration,
and then we have a Flame Chart.
I'll show you an example.
In this Call Chart here, method A makes multiple calls
to B, which makes a call to C. So there are multiple instances
of the same call sequence, A to B
to C, which means they can be aggregated.
To illustrate how to identify unique call stacks,
I have sliced this Call Chart for you.
And now we can start merging identical call stacks, just
like this.
And then finally, we flip the chart vertically.
And then we have a Flame Chart.
So let's compare the two for a moment.
The Call Chart on the left-hand side
retains timeline information, where it shows
exactly when something happens.
But the Flame Chart on the right-hand side
shows you aggregate duration information,
which makes it easy for you to discover long-running paths
in your code.
So the flame chart is helpful for looking at the big picture.
But sometimes you want to find the precise timing information.
And that's why we need a Top Down tree.
Constructing a Top Down tree is trivial.
You just start from the caller, keep
adding its callees as tree nodes until you have traversed
the entire Call Chart.
And then you have a Top Down tree.
For each node in this Top Down tree,
we're interested in three pieces of timing information.
Self Time-- the time it spends executing
its own code, Children Time-- the time
it spends calling into other methods, and Total Time,
which is the sum of the two.
With the Top Down tree here, we can easily
incorporate all three pieces of timing information
in one table just like this.
So I'm going to walk through an example here
to show you how the timing info is computed.
On the left-hand side is the same Flame Chart example
we've been using.
On the right-hand side, we have a Top Down tree.
Let's start with the root node A. A spent one second
executing its own code.
So the Self Time is one second.
And then it spent a total of nine seconds calling
into methods like B and D, which means
the Children Time is nine.
That gives us a total of 10 seconds.
Still with me?
I'm going to pick up the pace.
Now, if I expand the node A, we're
going to see two of its colleagues,
B and D. B spent three seconds executing its own code
and four seconds calling into other methods.
That makes a total of seven seconds.
D here is a little bit interesting.
All it does is call in to C, which
is common in wrapper methods.
So the Self Time is zero.
And then both Children and Total Time are two.
So notice A's Children Time is the sum of B
and D's total time, which makes sense.
Now, this is what the table will look like when fully expanded.
Don't worry, the CPU Profiler will compute everything
for you.
I'm just using this example to show you
what these numbers mean.
So if we pause for a moment and compare these two charts,
the Flame Chart on the left-hand side
is good for visually checking where your code is spending
a lot of time on, while the Top Down
tree on the right-hand side is good for looking
at the precise numbers.
And also being a table, this allows
you to sort by individual columns, which
makes it extremely useful.
So a Top Down tree is very cool, but sometimes you
find yourself going down the call
stack until you find a particular method.
That's where a Bottom Up tree shines.
So as the names suggests, the Bottom Up tree
starts from the bottom.
So we can construct this tree by going backwards
and keep adding a node's callers to the tree just like this.
But wait, there's more.
For every unique node, we can construct a Bottom Up tree.
So yeah, I lied.
It's a forest.
Let's do one more math problem and figure out
the timing information.
So first thing we notice is, on the right-hand side,
we have four rows because we have four trees in this forest.
Let's start with node C. Self Time
is just time is spent executing its own code.
4 plus 2 is 6 seconds.
C doesn't call any other method.
So Children Time is zero.
That gives us a total of 6.
When you expand C, its callers, B and D, will show up.
What do these numbers show?
Is this what you expected?
No?
Remember, we're looking at C's Bottom Up tree.
So all the timing information in that Bottom Up tree
is time spent by C. So what we are counting here
is not B's Self Time, but C's Self Time
when called by B, which means that's four seconds here.
And similarly for D, that's two seconds.
And then since B and D are the only methods calling C,
they should add up.
Now, let's move on to the next tree,
which is B's Bottom Up tree.
The Self Time is trivial-- three seconds.
The Children Time is the time that's
spent on calling other methods, which is just C here.
And that's four seconds.
And Total is easy.
It's always Self plus Children.
So this is what the table will look like when fully expanded.
Again, our CPU Profiler will take care of everything.
I'm just trying to use this example
to make sense of these numbers.
So here's a real-world example of how the Bottom Up trees
can be helpful.
Sometimes you want to find how a method is called.
For things like the Nano Time here, it's
called by many methods.
So you want to use the Bottom Up tree where
you can find any node expanded, go
through the list of its callers, and then find
the suspicious one that you may be interested in.
I know it's a lot of information.
If you forgot about all the math, it's OK.
Here's a cheat sheet I prepared for you.
So Shukang and I will be at the Android Studio Sandbox
after this.
If you have any questions, find us
at the Sandbox on the second floor.
Thank you very much.
[MUSIC PLAYING]