The source for the project is at Github:
https://github.com/angjelkom/flutter_2048
In this last part of the “Making 2048 Game in Flutter” series we will see how we can use the Debug, Profile and Performance tools to find Animation Junks in Flutter.
First of all what is an Animation Junk? To answer this question we need to ask another question: What is an Animation?
Animation it’s a technique in which multiple different are shown on the screen every second which gives us the illusion that the image is moving. The more technical term for it is “Frames per second” or FPS for short for those of you gamers.
In cinema the movies normally run at 24 FPS so 24 images (frames) run every second, but in Video Games we see higher FPS some games don’t have limit and the game runs on the FPS the device is supported.
Apart from games mobile applications also run on certain FPS depending on the device. Most of the devices have 60 FPS screens but nowadays there are devices running at 90 and 120 frames per second.
So to explain what an Animation Junk is, let’s take the devices with 60 FPS for example. In translation 60 FPS means that each frame needs to be rendered every 16.67 ms approximately. If a frame takes longer then the Flutter Engine needs to skip it and render the next frame, and that’s what Animation Junk means. If animation takes longer than 16.67 ms to render that animation is junk and needs to be skipped which to the end user will look like the animation is glitching or hanging.
Now if you followed the tutorial so far the game you’ve made doesn’t have the performance issue I will talk about in this tutorial, but it will be a great lesson to help you understand how to use the tools Flutter provides to find such issues.
When the game was almost finished I started testing the app in release mode
You can test the app by running flutter run --release in terminal
Initially it ran great, but the more I played the more I noticed some sort of lagging or hanging and I could repro that only after playing the game after some time, and closing and re-running the game didn’t had the issue anymore but the more i started playing the lagging would show up, which obviously it looked like memory leak.
So first thing I did is I ran the game in profile mode you can do that using either vscode by clicking the Debug tab and clicking the “Show all automatic debug configuration” link, selecting Dart & Flutter and selecting the profile mode option.
Another way is to run the flutter run --profile command in terminal.
The difference between the final app you’ve made in the previous lesson and the version that had the performance issue was this.
Instead of creating and passing the CurvedAnimations from the game.dart widget, I was creating them inside the AnimatedTile widget.
Now that obviously looks like a bad idea, but at the time it didn’t sound like a bad idea, considering that most of the tutorials online suggest same or similar approach, and even if they don’t it looked like the right thing to do.
The components/animated_tile.dart looked like this:
So instead of passing the CurvedAnimations I was passing the AnimationControllers to each AnimatedTile Widget and create the CurvedAnimation for each Tween.
So after thatI’ve ran the game in profile mode and tapped the performance icon (the last icon with the pulse icon):
Note if you want to use the devtools instead, open the command palette in vscode and type devtools
To open the command palette in vscode press CTRL + Shift + P (Windows/Linux) or CMD + Shift + P (Mac)
Initially while testing the game the time frame looked like this:
which was expected, the frames took less than 16.67 ms to render on my S10 Plus which has a 60 frames per second screen. But after few plays the the frames took longer and longer to render, and after a while I was seeing this in the time frame:
The first thing that crossed my mind was a memory leak, so I looked at the timeframe to see what exactly takes so long:
as you can see from the screenshot above the issue was in the animation, not in the build process, not in the layout or paint process but in the Animator.
So to investigate more the first thing I did is of course dispose of the animation controllers and re add them. To be able to test this nicely in profile mode I’ve added a floating action button which would dispose and recreate the animation controllers when the button was pressed.
So after reproing the junk animation I’ve pressed the button, and as expected the frames “normalized” and the animation didn’t junk for some period.
Next I checked the source of the AnimationController specifically the dispose method to see what exactly was being disposed by the AnimationController.
According to the source here apart from the ticker being disposed the Listeners and StatusListeners get disposed.
Initially it didn’t make sense for the Listeners and StatusListeners to be the cause as I’ve added only two status listeners one for each AnimationController. So to test further I’ve first tried calling clearListeners() for the animation controllers instead of dispose, which of course didn’t solve the issue, and then I tried calling clearStatusListeners() which surprisingly “normalized” the frames rendering like dispose did before.
So the problem was in the status listeners, but how is that possible as we only have two status listeners, one for each AnimationController.
To figure that out I’ve ran the app in debug mode and debugged the source code of the animation controller. Specifically I was debugging the AnimationLocalStatusListenersMixin which is a mixin holding the list of listeners and status listeners. Thanks to that I’ve discovered that for each round (each time I swiped and tiles moved) the list of status listeners kept increasing and that every time an animation is being rendered in flutter an “initial” status listener is being added.
But that again didn’t explain the large amount of status listeners for 2 reasons:
- The move and scale AnimationControllers are created in the TileBoard widget, so during the game they were created once, so that means one status listener being added initially, apart from the other one we’ve added ourself.
- Again because the controllers are being created in the TileBoard widget, it was not possible for round to trigger a new status listener to be added, especially in the amount it was being added each round.
So obviously the AnimationControllers weren’t the cause. So i continued debugging and watching the amount of status listeners and from where they were being added.
That’s when I noticed that a status listener was being added for the CurvedAnimation, that made me remember that CurvedAnimation and the AnimationController both extend from the Animation<double> class and are basically Animation<double> objects.
Realising this, the issue with the amount of status listeners made total sense.
The CurvedAnimations were being created inside the AnimatedTile widget. And here are creating CurvedAnimation for each tween so three CurvedAnimations being added. But that’s 3 CurvedAnimation per AnimatedTile, so every time the AnimatedTile was being recreated by the parent widget, three CurvedAnimations were being added, and for each CurvedAnimation an “initial” status listener was being added.
Overtime that amount was so large that the Animation started lagging because the engine needed to inform all those status listeners.
But that asks another question, why did the frames “normalize” when the controllers were being disposed, as we didn’t dispose the CurvedAnimations specifically but the controllers, so why did it still worked?
If we have a look at an example for the CurvedAnimation:
the CurvedAnimation has a parent parameter to which we pass the AnimationController, which is why the dispose of the AnimationControllers worked, because disposing of the AnimationController which is the parent disposes of all of the children Animations.
So the solution to this is like the initial tutorial, have the CurvedAnimation being created in the TileBoard widget so they get created once and not for each AnimatedTile Widget and not each time the AnimatedTile widget get’s recreated, and pass the CurvedAnimations instead of the AnimationControllers to be used by the Tweens in the AnimatedTile Widget.
Lastly you might be wondering, could’ve I avoided so much debugging? Could’ve I used some other Flutter Tool which would’ve helped to locate the problem apart from the Performance tool?
Well the answer is yes. Flutter has few tools apart from the Performance tab:
You can access the list by either opening command palette or press the “Dart DevTools” button on the bottom right of the VSCode window.
In this specific case the Memory Tool would’ve been helpful to specifically locate the issue.
If we open the Memory Page we will see this:
Specifically I could’ve taken advantage of the Allocation feature:
The Allocations Tab allows us to track changes in memory instances specifically classes being constructed. So we could’ve used this tool to see how many times some class has been constructed, and we can use the Search bar on the right side to look for the specific class.
So in this example, again let’s say we do few rounds in the game, and after some time we again see spikes in the time the frame needs to render.
Now if we go to the Memory Tab and switch to the Allocations tab and press the Track button we will see lots of classes:
Now let’s search for the AnimationController, the search bar is smart enough to auto suggest a completion.
Let’s select the AnimatedController and check the checkbox on the left side. And also let’s do the same for the CurvedAnimation.
Now that we have both checked we can order the list by the Track column which would give us the Tracked classes:
And as we see here the amount of CurvedAnimation instances created is a lot.
But again seeing it now makes more sense than it did back then. Also because I’ve added the CurvedAnimation to be created inside the AnimatedTile I could’ve probably considered to be fine that there are more CurvedAnimations than AnimationController, but as the number increased with each round it would’ve been more clear that something isn’t right with the CurvedAnimation.
Lastly to give you another example on how this could’ve been solved is by keeping the CurvedAnimation inside a stateful widget that was a parent of the AnimatedTile widget and making sure the CurvedAnimation was being disposed. But of course that’s not a great solution and having the CurvedAnimation created once for each controller is better.
So apart from having the CurvedAnimation being created inside AnimatedTile each time the AnimatedTile was re-recreated, a CurvedAnimation was being created for each tween separately so that’s three CurvedAnimations for each AnimatedTile and on top of that the CurvedAnimations were never disposed which made the performance worse each round.
That’s all for this parts or series. I have to admin writing and polishing these posts took me longer than the actual project, but it was worth it in the end.
Lastly I’ve compared the original 2048 game with the one we made here, and of course it’s not a proper comparison as many factors are involved in at how many FPS the game runs and on top of that this is not a heavy graphics game but it’s still awesome to see the game running at 60FPS constantly compared to the original running at 50–60FPS.
The source for the project is at Github:
https://github.com/angjelkom/flutter_2048
Hopefully you will find these tutorials helpful, and helped you learn how to use animations in Flutter, manage the state, saving the state, solving performance issues, and how to make simple Flutter Games in the process.