Mastering Dart & Flutter DevTools — Part 6: CPU Profiler View

Flutter Gems
13 min readDec 28, 2022

by Ashita Prasad (LinkedIn, Twitter), fluttergems.dev

This is the sixth article in the “Mastering Dart & Flutter DevTools” series of in-depth articles. In this article, you will learn how to analyze an app’s CPU usage using the DevTools’ CPU Profiler View. In case you want to check out any other article in this series, just click on the link provided below:

Installation and setup of DevTools is a pre-requisite for this part. In case you missed it, the installation and setup details are provided in detail here.

With more that seven million apps available across iOS and Android stores, it is difficult to get downloads, but even harder to keep users around. Performance is the key to retaining users as according to a study more than 80% of people in US delete or uninstall an app because of performance issues. While 38% of these users switch to another app, 34% just abandon their task entirely and the possibility of getting a second chance is next to nil. An app’s CPU usage has a direct impact on its performance, so if it’s code is optimized to consume lesser CPU resources then it will:

  • Provide faster & smoother UX (User Experience),
  • Conserve battery, and
  • Not lead to any device heat-up issues.

Profiling is a well known performance engineering method used for analysing the CPU resources consumed by a code and it can be performed using the CPU profiler tool that comes pre-packaged with DevTools.

In this article, we will perform CPU profiling of a real-world Flutter app using the DevTools’ CPU Profiler View. After which, we will use this tool to investigate the CPU usage and identify the cause of its low performance. Then, we will optimize the code to reduce the CPU usage and verify it using the CPU Profiler View.

CPU Profiler View

CPU Profiler View provides insights into the CPU usage of an app, so that one can investigate any performance issue that might exist due to long code run-times. Once the “culprit” function or method is identified, the code can be optimized, thereby reducing the CPU usage.

To showcase various aspects of the CPU Profiler View, we will profile a Flutter app that generates Mandelbrot Set visualization, a problem that is compute intensive in nature. The app we are going to profile is a modified version of Thomas Burkhart’s project on isolates. In case you want to understand the inner workings of the code, make sure you check out his Flutter Vikings talk.

Now, let us proceed and get started.

Step 1: Getting the Source Code

Create a local copy of the below repository 👇

Open the repo in GitHub as shown below.

Click on the green button <> Code, then visit the Local tab and click Download ZIP to download the source code for this exercise.

Extract the file using any unzip tool.

We now have a local copy of the source in the folder flutter_mandel-master.

Step 2: Opening the Project in VS Code

Launch VS Code, open the project folder in VS Code and go to pubspec.yaml file.

Click on Get Packages to fetch all the packages required for the project.

Click Get Packages

Step 3: Launching CPU Profiler View in VS Code

To perform CPU Profiling, an app must be run on an actual device. So you can connect an iOS or Android device. In case it is a desktop app and the development desktop device is also the target platform, then the app can be compiled and run in profile mode on the desktop.

Click on No Device in the status bar and select the target device. If a default device is already selected, you can click on it and change the target device.

Select Target Device

We will be building a macOS desktop app for our use case, so let us go ahead and select the target device as macOS. You can also choose Linux or Windows based on your OS.

Select macOS as Target Device

Now, run the app (main function) in Profile mode as shown in the image below.

Click “Profile” to run the app in Profile mode

Click on Dart DevTools in the status bar and then click on the Open DevTools in Web Browser command.

Click Dart DevTools
Open DevTools in Web Browser

Now click on the CPU Profiler tab as shown below.

Click on the CPU Profiler tab

Once we click on the CPU Profiler tab, we can observe a (largely empty) dashboard as shown in the image below.

The CPU Profiler tab

As shown in the image below, there are three primary buttons — Record, Stop and Clear that can be used to start CPU profiling, stop CPU profiling and to remove all CPU usage data, respectively.

Top-Left Buttons: Record, Stop and Clear

There are also some more CPU profiler options available on the top-right as shown in the image below.

Top-Right Buttons: Other CPU Profiler Buttons

These options are:

  • Profile app start up: This button loads all CPU samples that occurred before the first Flutter frame was drawn. In case there is a delay in app startup, this helps in identifying the underlying functions that cause the delay. To learn more about app startup and how it is important to have a low startup time, you can check out this article.
  • Load all CPU samples: This button loads all available CPU samples from the profiler that includes data pertaining to remaining isolates apart from the main isolate. We will look into this feature later in this article (in Scenario #2).
  • Profile granularity: The default (medium) rate of collection of CPU samples is 1 sample per 250 microseconds. Based on the requirement, this setting can also be modified and set as 1 sample per 1000 microseconds (low granularity) or 1 sample per 50 microseconds (high granularity).
  • Export: The collected CPU samples data can be exported using this option.

As the app is up and running and the CPU Profiler is ready to collect the CPU samples, we will take a look into two scenarios in this exercise:

  • Scenario #1 — In this scenario, CPU profiling is performed when the entire app is executed in the main isolate (thread). Using the CPU Profiler View, the collected CPU samples will be visualized and analyzed. Finally, the root cause of the low app performance will be identified with the help of this tool.
  • Scenario #2 — Subsequently, in this scenario, the problem identified in Scenario #1 is resolved by employing parallel processing using isolates. This improves the app’s performance and the new CPU profile is visualized for all the isolates (main isolate + spawned isolates).

Scenario #1

Let us start with the first scenario.

Run the app in Profile mode.

Home Screen of the app after launching it in Profile mode

Open Dart DevTools and start CPU profiling by clicking on the Record button as shown in the image below.

Start recording CPU samples

As shown in the image below, click on the Launch Explorer button on the app’s home screen. The Mandelbrot set will be computed and the visualization will appear on the next screen.

Click Launch Explorer

Now, let us click on the “Zoom In” floating button 3 times as shown below.

Press Zoom In button thrice

Go Back to the browser tool and hit the Stop button to stop the CPU profiling. The CPU usage data is now transmitted, processed and displayed.

The collected samples are analyzed and presented in the following three tabs:

1. Bottom Up

The Bottom Up tab (as shown in the image below) shows the list of functions or methods (along with their source file location) that are called last in the call stack for a given CPU sample. These functions are ordered by their execution time, with the longest running function at the top of the table. Apart from absolute numbers, the execution time of these functions are also shown as a percentage of total runtime which is useful for identifying the functions/methods that need our urgent focus.

Bottom Up Table

2. Call Tree

The Call Tree (as shown in the image below) is a top-down representation of the entire call stack, that makes it exactly opposite to the Bottom Up table. One can expand a method by clicking on it to show the methods it is calling.

Call Tree

3. CPU Flame Chart

The CPU Flame Chart (shown in the image below) visualizes the timeline of CPU usage using a top-down stack trace. This is presented as a stack of method calls where each method calls the method below it and the width of each method call is equal to its run-time.

CPU Flame Chart

In this Flame chart, the methods belonging to core Dart and Flutter libraries are coloured Lavender Blue, and user defined methods and methods belonging to 3rd party packages are coloured in Pale Orange. This differential colouring also helps us in quickly identifying the part of our code where performance bottleneck might be occurring so that we can make the requisite performance improvements.

Identifying the CPU Intensive Method

In the current scenario, looking at the Bottom Up table or the CPU Flame Chart has revealed that iterations function (code below) of Mandelbrot class is CPU intensive in nature and it consumed ~75% of the total scenario execution time.

int iterations(double x0, double y0) {
if (y0 < 0) {
y0 = -y0;
}
double x = x0;
double y = y0;
double xT;

for (int i = 0; i < maxIterations; i++) {
if (x * x + y * y > 4) {
return i;
}

xT = x * x - y * y + x0;
y = 2 * x * y + y0;
x = xT;
}
return maxIterations;
}

This function returns the number of iterations of computation required for a point (x0, y0) in the complex plane to exceed a particular threshold value. The CPU usage of this function is high as it is called by renderData function (code below) for each and every pixel on the app screen.

  renderData({
required List<int> data,
required double xMin,
required double xMax,
required double yMin,
required double yMax,
required int bitmapWidth,
required int bitMapHeight,
}) async {
// Per-pixel step values
double dx = (xMax - xMin) / bitmapWidth;
double dy = (yMax - yMin) / bitMapHeight;

double y = yMin + dy / 2;
int ib = 0;
for (int iy = 0; iy < bitMapHeight; iy++) {
double x = xMin + dx / 2;
for (int ix = 0; ix < bitmapWidth; ix++) {
int iters = iterations(x, y);
data[ib++] = colorFromIteration(iters);
x += dx;
}
y += dy;
}
}

There are two ways using which we can improve the performance of any code:

  • Firstly, by reducing the time complexity of the problem.
  • Or, by breaking the problem into smaller sub-parts that can be executed in parallel.

In this case, there is no scope of reducing the time complexity, but we can perform parallel computing by sub-dividing the complex plane and performing the iterations calculations for all the regions in parallel.

How can we do it in Flutter?

Well! The answer lies in the next scenario.

Scenario #2

In a Flutter app, all the Dart code runs in a single isolate called main that has a single thread of execution (i.e., all events run in a single thread). But, in case we have some heavy computational load, we can create more isolates that can help us with parallel code execution on multi-core processors. This greatly improves the app performance as now we can fully utilize the capability of the device CPU.

Revisit the app and take a look at its home screen (as shown below), you can see that # of rendering isolates is set as 0 by default. This means that the mandelbrot set compution code runs in the main thread and no separate isolate is spawned.

Default App Home Screen

Increase the # of rendering isolates count to 4 by clicking the + button as shown below. This means that apart from the main isolate, 4 new isolates will be created that will compute the Mandelbrot set for the divided complex plane.

4 isolates are spawned to divide the computational workload

Click on Record and repeat the steps we performed for Scenario #1, i.e., click the Launch Explorer button to view the Mandelbrot set. Then, click on the “Zoom In” floating button 3 times as shown below and finally click on the Stop button.

Repeat the steps we performed in Scenario #1

You can observe that the app performance has improved more than 2-fold. Let us now validate our observation quantitatively using the CPU Profiler View.

If we take a look at the Bottom Up table as shown below, the iterations method of the Mandelbrot class is now missing which was present in Scenario #1.

Bottom Up Table for Scenario #2

This is because we are looking at the CPU profile data of the main isolate that is visible at the bottom of the CPU Profiler View.

CPU Profile of the main isolate

This option is known as Isolate Selector and it can be clicked to view all the isolates that were spawned.

Isolate Selector

Let us click an isolate (IsolateEntry.isolateHandler #2) to view its CPU profiler analysis as shown below.

Select Isolate

It appears the CPU profiler only works for the main isolate and no data is captured for the spawned isolates. 🤔

Where are the CPU samples data for the isolates??

CPU samples are being captured for the isolates and can be loaded by clicking the Load all CPU samples button as shown below.

View isolate #2 CPU profiler data

In the Bottom Up Table for IsolateEntry.isolateHandler #2, we can now observe the runtime of the intensive computation Mandelbrot.iterations that was delegated to the spawned isolates.

Bottom Up Table for one isolate

Based on the section of the complex plane assigned to an isolate, the computation required varies hence the runtime of Mandelbrot.iterations for each isolate varies (88 milliseconds, 98 milliseconds, 111 milliseconds, 137 milliseconds). As these isolates are running in parallel, there might be a slight offset in the process start time (~5 milliseconds), still there is a significant performance improvement [~357/(137+5) = ~2.5 times] as it took 357 milliseconds when Mandelbrot.iterations was running serially in the main isolate in Scenario #1.

This brings us to the end of Scenario #2, where we walked through the process of using CPU Profiler View while running a Flutter app that improves its performance by utilizing multiple CPU threads using isolates.

In this article, we took a case study based step-by-step approach to learn the CPU Profiler View, a useful tool for profiling CPU usage and visualizing the results while running a Flutter app. We looked into two scenarios:

  • In the first scenario, we got familiar with the CPU Profiler View interface and used it to investigate and identify the cause of app’s low performance.
  • In the second scenario, we tackled the identified issue using parallel processing by spawning isolates. Using the CPU Profiler View, we were able to view the CPU usage information of not just the main isolate, but also the spawned isolates. Finally, the performance gain was analyzed quantitatively with the help of this tool.

We would love to hear your experience with the CPU Profiler View and any other suggestions in the comments. In case you faced any issues while going through this exercise or while running the tool for your project, please feel free to mention it in the comments and we can definitely take a look into it.

In the remaining articles of this series, we have discussed other tools available in the DevTools suite that can help you build high-performance Flutter apps. Don’t forget to check out the links below to navigate to the tool you want to learn next:

Source Code(s) used in this article:

Special thanks to Kamal Shree (GDE — Dart & Flutter) for reviewing this article.

--

--

Flutter Gems

Maintained by Ashita Prasad, Flutter Gems is a curated package guide for Flutter ecosystem. Visit https://fluttergems.dev