[MUSIC PLAYING]
DAVID HERMAN: Hey everyone.
I'm David Herman.
JUSTIN NIETO: And I'm Justin Nieto.
DAVID HERMAN: And we are developers on Android Studio.
Now, earlier this year at I/O, other members of our team
gave a Tips and Tricks talk about editing
your project in Android Studio.
It is excellent.
However, as developers, we all know
that there are some days you just
spend way more time in your debugger
than you do in your code editor.
So to that end, Justin and I-- we went around our team.
We asked everybody for their favorite debugging features.
We don't have a lot of time up here,
so we narrow them down to what were the essential tips that we
think will be easy to incorporate
into your debugging flow.
Now, in order to showcase these features,
we put together a simple game inspired by the new Android 10
version.
Rules simply are you grab 10's, you get points.
You run into too many desserts, which represent old versions.
It's game over.
The core of this game is a loop that
tries to run at 60 frames a second, where
it updates all of the objects in the game,
and then it renders them.
I'm going to assume that your own app is not
quite the same as this one.
But don't worry, all of the features
that we're going to share here will apply for any project.
Now, this is all going to be a demo from this point forward.
And we have a lot to get into, so I'm
going to get out of the way and kick it over to Justin.
JUSTIN NIETO: Right, so as David mentioned,
we're going to talk about a lot of debugging tips.
But if you're anything like me, you're
going to sit through this whole presentation
and then go home and still use printf statements everywhere
to see what's going on.
That's OK.
So we have tips for you too.
We're actually going to start here.
So if we open up Logcat, which is where all of our Android
logs surface in the IDE, you'll see that when we start the app,
we have two things going on here.
We're logging the FPS--
the frames per second rate--
every second.
And then when Android dies at the end of the game,
we also log the score that you got through the session.
So you might notice right away that there's kind of some bloat
here-- things like the date and the threads--
that we don't really care about.
One thing you might not know is that you
can go to this Settings bar over here
and remove some of this information
so that we can cut it down to just the tag and the message
that we care about.
[APPLAUSE]
Right?
Yeah.
So even with this, you might notice that we
have all of these spammy logs.
And they're kind of getting in the way of us seeing
these high score messages.
So what we can do there is actually search
here to filter things down.
And this is nice, but if this is a super useful search
that you do all the time-- or you have multiple searches--
you might want to actually save this
so you can come back to it later.
So what we can do to make that happen
is to go here and add a custom filter.
So if we go over here in Edit Filter Configurations--
I did this before.
So in this case, I'm looking at high scores.
I can type in the substring that I'm looking for.
And now I have the same filtering effect.
but I can jump back and forth between different ones.
So for now.
I'll go back to the whole log set.
So that's great, but maybe we really
want to look at every log in the game.
We just don't want these spammy FPS logs.
So what we can do here is highlight the substring
that's relevant between all these different spammy things
that we don't care about, right-click on it,
and go to fold lines like this.
And creates a filter for us that will
collapse all of these messages together and leave
just the things we care about.
So if I do this, you'll see that now we're
just looking at the scores and some other information
from the emulator.
And if we need to look at this information later,
we can always expand it or collapse it.
So now that we've got all the logging bits out of the way,
let's go ahead and jump into some code.
In particular, we're going to start
at this loop that happens every frame of the game loop,
where we go through all the items and reason over them.
So since it happens every frame, I
should be able to set a breakpoint here.
And when I start debugging, this should hit pretty immediately.
So let's go ahead and do that.
So I didn't do that.
Just kidding.
So I did this on purpose.
I hit the Run button when I started the app, instead
of hitting the Debug button.
And while I did it on purpose to demonstrate the next feature I
want to show, this actually happens
to me quite a bit in real life.
And it can be super frustrating to stop the app,
start it in Debug mode again and then
work your way all the way back to the state
that you're trying to debug from.
So what you can do instead is click this icon over here
and attach the debugger to the already-running process.
And when I do that, you'll see that we hit the breakpoint
pretty immediately.
So that being said, we have our breakpoint.
But say the bug that we're tracing down
is related to when Android actually
hits one of these desserts.
It's not going to have a breakpoint right
here because it gets hit every frame.
And we never get up to the dessert
that we're trying to hit to repro.
So instead, what we might want to do
is go down to the part of the code
where we've actually determined that we've hit a dessert.
One thing that's nice, because I don't need this breakpoint
anymore, I can just drag it to the line that I care about.
And it gets rid of it from the old line, and then I can run.
And when I collide, now I get it on collisions.
That's great, but maybe the bug that I'm actually
chasing down is only related to the last dessert that I hit--
when Android dies at the end.
So for that, we had the same problem.
I had to stop every time I hit a dessert.
It gets tedious.
So what we can do is what's called setting
a conditional breakpoint.
So I can right-click on this breakpoint in the debugger,
add my condition.
And I can put any Boolean expression in Kotlin here.
And if it evaluates to true, when the code hits it,
then we'll catch at the breakpoint.
So in this case, I'm going to type player.health
is equal to 1 because we're about to decrement it
for the last time in the case that we care about.
And when I go through that--
we hit twice and we didn't catch.
Now I hit the last one and I've caught the break point.
And I only get the case that I care about.
So to take this example even further,
say the bug that we're tracing is actually
last dessert that you hit, but when we render the hearts.
So if I jump down into our render loop
where we make that determination between broken and full hearts,
I could set a breakpoint here.
And because I've already hit the other conditional breakpoint,
when I run, it'll go through to the rendering step.
And that's fine.
But if I want to check this condition multiple times--
I want to cycle through this in case
the bug doesn't repro every time--
this isn't a great thing to do to leave
this unconditional breakpoint here.
Because I have the same problem.
Every frame, I'm going back through.
What I can do to get around this is,
right-click on the breakpoint point again,
open up the More menu.
And go to this section here, where
we can disable the breakpoint until the first one
that we had was hit.
I'll let the game run through.
Now we end up with, I'm not stopping every time
because the breakpoint is disabled.
And then when I hit the last dessert,
our first breakpoint catches.
And because we've hit the first breakpoint,
it's activated the other dependent breakpoint.
And when I run through, we land there in the render loop.
So what you might have noticed during all these things is,
when we right click on these breakpoints
and start working with them, you'll
notice the Suspend tab here.
And so far we've had All selected for all of this.
And that means that when the currently-executing thread
encounters the breakpoint, we will just
stop every thread in the application.
And that pauses the whole game state.
And that can be useful for the bug,
like we were talking before with rendering the hearts.
But if you're working on a multi-threaded application
and you're chasing down some really tricky synchronization
issue, you might also want to try
suspending just the thread that encountered the breakpoint.
So to highlight this functionality,
we have a section of the game that's
responsible for when Android dies,
simulating sending off the score to some server somewhere.
In our case, we just delay and log some stuff.
But if I set a breakpoint here in the background thread where
we're doing our network call, right-click it and tell it
to suspend on just the thread, then what I can do is play
through my game--
which is over again--
and you'll notice that because I died before, I'm already
in the background thread.
But the UI thread is still active,
which means I can cycle through the whole game
again and get myself in a state where, say, I have
multiple network calls cued.
And I can try to reproduce some kind of tricky synchronization
issue there.
So for the next tip I want to show you,
the rest of the breakpoints that we have set
aren't particularly useful.
So I'm going to go ahead and try to disable them here.
So what I can do to do that is right-click
and then deselect Enabled.
And that's fine.
But what might be handier is to use
Alt + Click, which will just toggle these guys on and off--
or Option + Click if you're on Mac.
So all that being said, I'll go to my last tip, which
isn't that line, it's this one.
So this is the part of the code where we determine the score
multiplier, which is this guy up here that
determines how much your points are multiplied
by when you hit a 10.
Let's say I need to track this value over all sorts of times
in the app--
very, very many times.
I can set a breakpoint here.
And I can run through my app.
And we have the same problem.
When the game is running, I hit this guy all the time.
And then I have to chase down the information
I'm looking for through this whole menu, which
can be tedious.
What I really want to do is log the multiplier value here.
But I don't want to drop log statements all over my code.
It's kind of a pain.
So what we can do instead is leave our breakpoint
and disable suspension altogether.
So now, whenever we hit this breakpoint,
we're not going to stop the app.
Then I can go over here to this Evaluate and Log section.
And I can type in any Kotlin expression.
So in this case, my multiplier is score.multiplier.
And now when the thread encounters this breakpoint,
it doesn't stop.
It just evaluates this expression,
logged it to the console, and moves on.
If you don't need that much information-- you just
need to check to see if we go through a certain code path--
you can deselect this and instead
choose to have a breakpoint hit message.
And it'll just tell you that you hit this breakpoint
at this time.
And in fact, that's so handy that there
is a shortcut for it.
If you Shift + Click in the gutter,
it'll create one of these for you.
So those are all the tips that I wanted to share.
I'm going to invite David back over
to tell you about some of the ones that he knows.
[APPLAUSE]
DAVID HERMAN: All right, have you ever
found yourself working on a bug where you're not
making progress, you decide to put it aside for a bit,
and then work on a second bug?
But while you're working on that bug,
you keep hitting the breakpoints from the first bug.
You're not ready to delete those breakpoints yet,
so you just keep disabling one at a time.
Well, it turns out there's a better
way using breakpoint groups.
So I'm going to go here.
And let's imagine that this was a breakpoint we just hit.
Right-click on the breakpoint, go to More.
And now you see all the breakpoints up here.
You can multi-select them, right-click,
create a new group for them.
You can name this after the bug that you're working on.
I'm just going to call this Justin.
Now that they're grouped, you can enable them--
toggle them on and off with a single click.
And when you're done with the bug,
could also just delete all of them as well.
I'm going to jump back into the code.
Now for some context, this is the function
responsible for initializing the current items.
So I'm going to let this run a couple of times
so you can see it.
So when the item is off the top of the screen,
we don't yet know if it's going to be a random dessert
or a number 10.
Now, let's say here, I want to step
into this generate random type method,
but I accidentally step over it.
Now, I may have stepped over on purpose also,
but maybe the result was a little bit confusing to me.
I didn't expect it.
So what I wish I could do in retrospect was step into it.
If you're targeting a device that's
running at least Android 10, you can click on this drop frame
button.
And what that's going to do is it's
going to pull you out of the current method
and put you right back before it started.
So when you step into it again, you get your second chance.
Now, this feature is not a time machine.
So if I was in the middle of a long function and it had done
a lot of intermediate work-- for example,
modified the state of this current class--
that doesn't get undone when you drop frame.
That's not a deal-breaker, it's just something to be aware of.
And despite that, this feature has saved me a ton of times.
Now I'd like to bring your focus down here to the Variables
window.
So this is the item class.
And this particular instance of an item is ID10231.
So as you can see on the screen, there's
already a couple of items there.
But let's say I just want to follow the lifecycle of just
this particular item.
Now, what I might have done in the past
is write down 10231 on a piece of paper,
but that's a little error-prone.
Instead, just right-click on it, mark the object.
It asks you for a label.
I'm going to creatively call it my item.
And immediately you'll notice that anywhere
that this appears in any of these debugging windows,
it's labeled.
Now my favorite part of this is, if I come over here
to a random method--
oh by the way, did you know you can click on the line gutter
and you'll run to that directly?
So now I'm in the render method.
And my item instance, of course, is not in scope.
What I could do is come over to the Watches window.
And I can type my label, my item,
followed by underscore debug label.
But you don't have to worry about remembering it
because it auto-completes it for you.
And anywhere in my app, I can actually
follow my single instance to see what it's state is doing.
This is actually very powerful when
combined with conditional breakpoints, which
we talked about earlier.
So this line here is responsible for rendering
all of these items.
So what I'm going to do is I'm going
to set a breakpoint on the line, I'm going to right-click on it.
And I'm going to set my condition to item
equals my item.debug label.
And when I run there, sure enough,
you can see that I am actually highlighting
my particular item.
And I didn't have to step over the breakpoints
of the other instances.
Now, as long as we're here and we're
at a breakpoint, what you can do is,
you can click on this Evaluate Expression button
to bring up the Evaluate Expression dialog.
It sometimes opens up here in single-line mode.
So I like to expand it to multiple lines.
Like the Watches window, you can type in any expression here.
So let's just do item.image.
And then hit the evaluate button and you
can browse the object in this Object Browser below.
But you can do richer expressions in here as well.
Like you can create variables, you can use if statements.
And let me just go ahead and verify
that this is working just by putting in a random expression.
And there you go.
So sometimes the variable and Watch windows
are great for when you're trying to watch
values change over time as you step through the code.
But the evaluate expression dialog
is really great if you just want a live inspection
shell that you can see the current state of your app.
And maybe you're not really sure what you want to debug,
but you can you dive in by doing these evaluations.
I'm going to jump back to a breakpoint
that Justin set earlier.
If you remember, this is the code
responsible for colliding with a desert.
And there's a condition on it.
As you can see, it's player.health equals one.
I'm going to enable it.
Now, this is going to be a little bit hard
to see in this case.
But I'm going to try running into some desserts.
And see if you can notice, there's a very slight stutter,
even when the breakpoint doesn't hit.
Let me go ahead and it's a little bit hard to see,
but now here on the third one.
So even in this case, if you couldn't quite see it,
whenever you have a conditional breakpoint,
even if you don't stop, the debugger
still has to do the work to evaluate that expression.
And if this was in the middle of a really tight loop,
this might become really noticeable.
Unfortunately, there's not really
much you could do in that case if this is actually
really affecting your debugging experience.
So what you just have to do is type the expression out
explicitly.
I'm just going to put a [? no op ?] here so I
could hook my breakpoint on it.
At this point, I've done the code version
of what the conditional breakpoint was here,
which I sometimes have to do for performance.
This is a point where you might decide to restart your app
and re-hit the Debug button.
But instead, let's go ahead and use Apply Changes, which
is available in all Android devices at least running 8.0.
Now, what this is going to do is,
it's going to apply my code patch over.
And I want to draw your attention
here to the Frames window because it now says Obsolete.
So what's happened is, I used to be
in this old version of the Update method.
And we've pushed over this new version with my new code in it.
But that being said, the debugger
is still stuck in the old one.
So what I'm going to do is, I'm going to use the Drop Frame
feature we talked before.
I'm going to go out of the old and step back into the new.
So here I verified that, in fact,
that the code has been patched because I hit the breakpoint.
I also want to call everyone's attention to this other button
up here.
So both of these buttons are Apply Changes.
This first one is responsible for patching just code.
The second one does the same thing,
but it also restarts the activity afterwards.
This can be really useful if you modified some layout resources,
or if the code that you're trying to debug
is, for example, in an onCreate method.
And finally, despite all of these tips and tricks,
you are going to still write some bugs,
you're still going to publish them,
and you're still going to get bug reports.
So if you ever get a bug report that has a call stack in it,
here it just looks a bunch of text.
Just copy it, come back over here to Android Studio,
click on Analyze.
It's really tiny, but analyze, analyze stack trace.
And you'll see that it already found the value that
was in the clipboard.
All I have to do is hit OK.
It's going to drop my call stack into the console
below fully annotated.
So very quickly I can see that this
is my code base versus this is code that I might not
have to pay attention to.
And of course, you can click on the links
to quickly jump through your code base.
[APPLAUSE]
That concludes the demo.
JUSTIN NIETO: So we talked about a lot of different tips today.
And as we mentioned before, there
were still more that we couldn't fit into this talk.
So if you are watching a recording on this video,
please check the description.
And we'll try to include some helpful links there.
If you are here in person, come talk to us in the Sandbox.
And we'd love to expand on some of these more.
If we forgot your favorite debugging tip, please tell us.
Leave it in a comment and tell us in person.
Because we'd love to know what it
is to incorporate in our own workflow.
And with that, thank you all so much.
[APPLAUSE]