Unfortunately, it just so happens that we live in a world with a limited amount of resources. This applies to both natural resources and resources of computer equipment, especially mobile devices. For this reason, in most cases, software developers are forced to adapt to the hardware resources that most users' devices have when creating programs or games.
An application can be ideal in terms of writing code or building its architecture, but it can also be completely unusable and buggy due to improper and suboptimal use of system resources. Optimizing application resource usage is often critical for providing the best user experience. This is where profiling comes in handy.
Profiling and Android Profiler
Profiling is a method of analyzing program execution. It helps identify performance bottlenecks and inspect software consumption of system resources to optimize it and improve app performance.
Android Profiler is an Android Studio tool for real-time profiling of resources used by an application, such as CPU, memory, and energy.
To start profiling, you need to run Android Profiler by compiling the application on a physical Android device or in an emulator using the button on the top toolbar. If you have already compiled the application, open the Profiler on the bottom toolbar or go to the main menu "View" → "Tool Windows" → "Profiler" (don't confuse it with "Profile"). After that, you will see a window with four timelines, as shown below:
The topmost line displays the lifecycle of the current activity, as well as the actions performed by the user (touching the screen, flipping the device, pressing physical buttons, and so on). The next three, CPU, Memory, and Energy, show the usage of the respective resources at a particular point in time. Each metric has a separate profiling section with its tools and settings.
You can save it as separate sessions to explore and compare resource consumption data in different scenarios. To do this, click the "Start a new profiling session" button , select the device and then the process name of your application. Click the "Stop current profiling session" button
to stop adding data to the session.
CPU profiling
Your application's CPU usage data contains a wealth of information that can help you understand how optimally your application consumes the CPU resources, what methods are using it and how much, how long it takes, and so on.
Functions/methods displayed in Android Profiler may take significantly longer to execute compared to their actual execution time without using Android Profiler. It means that during data analysis, you should be guided by the relative duration of such functions/methods.
To start profiling the CPU, click on the CPU graph. You will see a list of all threads used by your application. When there is any activity in the application, the timeline of threads will be highlighted with a certain color. Each color corresponds to a specific state of that thread.
Green: The thread is active and using the CPU.
Yellow: The thread is active but is waiting for an I/O operation, after which it can complete its work.
Grey: The thread is sleeping and not using the CPU.
Record traces
A trace is a record of device activity over a specified period of time. For more detailed and in-depth CPU profiling, Android Profiler provides the ability to record all information about an application's execution and CPU usage. Depending on the type of information you want to capture, you can choose one of the following trace recording configurations:
1. System Trace (Trace system calls or Systrace). This configuration records detailed information with which you can see how the application interacts with system resources. In addition, the records contain the exact timings and duration of the state of the application's threads. Unlike other tools, Systrace provides target data such as CPU usage across all cores or other important information that Systrace automatically gathers, such as frame lifecycle.
It also tells us if a frame takes more than 16 milliseconds (1 s/60 fps = 16 ms per frame) and indicates what might be wrong and how we can fix it. This is one of the features of Systrace that is missing from other CPU profiling tools. On the other hand, although this information is very useful, it doesn't appear in the context of the application's call stack. In other words, we cannot match it with the code we have written. However, there are various cases where Systrace can be useful, especially if you are tracking a specific piece of code. In this case, you can add custom trace events — markers that indicate the given area that should be displayed in the call stack. As a result, you will be able to match the data that Systrace gives you with the code you have written.
2. Java/Kotlin Method Trace. The distinguishing feature of this configuration is that it traces each executed function, collects and compares timestamps at the beginning and end of each method call to generate method tracing data, including information about time and CPU consumption. So, all of these functions will be displayed in the call stack. However, this approach, as you might have guessed, has a significant drawback. Method Trace uses additional CPU resources that impact application performance, which in turn affects the profiling data, especially for methods with a short lifecycle. If you're monitoring places with a high density of calls, there's a high chance you'll get distorted runtime performance data for your application.
3. Callstack Sample (Sample C/C++ Functions). This configuration collects callstack samples of native methods at frequent intervals during application execution. Method Sampling is convenient because we can control the sampling interval ourselves. The longer the interval, the lower the overhead (additional use of CPU resources by the profiler). Obviously, the trade-off is that your trace is going to be less detailed, and you may miss out on important information between these samples. However, setting a smaller sampling interval can distort the application's performance data.
4. Java/Kotlin Method Sample — legacy (Sample Java Methods). The specified configuration captures the call stack at frequent intervals during application execution. After that, the Profiler compares the received data and shows information about timings and system resource usage by the application. Note that the main problem with this approach is that a method with a short lifecycle is likely not to be captured by the Profiler. To avoid this, use Method Trace or Systrace with custom events.
We recommend you check out another third-party profiling tool — nanoscope. This is an extremely accurate method trace tool that is deprived of the main drawback of the standard Method Trace tool — overhead.
Inspect traces
Now that you've figured out how to record traces, it would be nice to be able to "read" and inspect them correctly. Let's get started!
If you open the required record, on the top you will see the already familiar CPU usage graph, and just below the user's actions, the lifecycle of the activity, as well as a list of the threads used by the application. Each thread has its own Call Chart — a visual representation of methods and function traces of the application, the moments of their calls, the duration of execution, including their child elements. To open the Call Chart, click on the triangle next to the name of the thread.
The color of each method or function corresponds to its type:
Orange — belonging to system APIs;
Blue — native methods and functions;
Green — the call stack of your application's methods and functions (they are interested in the first place).
Let's take a look at the concept of method arrangement using this application as an example:
fun main() {
a()
b(true)
}
fun a() {
b(true)
b(true)
b(false)
c()
}
fun b(callC: Boolean) {
if (callC) { c() }
}
fun c() { ... }We have a main() function that contains and executes several sub-functions. The length of each of these functions corresponds to its relative execution time. The duration of a function's execution is the total execution time of its execution, plus the total execution time of its sub-functions.
Detailed trace data is located in the right window of Android Profiler. Let's take a quick look at each of the tabs.
The Flame Chart tab displays the Call Chart. However, it concatenates identical call stacks. For example, the functions b()1, b()2, and b()3 in the chart above have the same call sequence (main → a → b). Accordingly, they will be combined on the Flame Chart, just like c()1 and c()2. However, c()3 and c()4 have different call sequences, so they won't be combined. In addition, the items will be arranged in decreasing order of CPU usage duration.
This way of displaying traces is very useful in cases when you need to see the overall picture of the method execution duration, especially those that have a short lifecycle but are run very often.
The Top Down and Bottom Up tabs contain methods arranged in a tree-like manner. They are combined similarly, as we discussed above. On the right is a table that contains CPU usage data for each method and its children.
The last tab, Events, contains information about all calls in the selected thread. By sorting the list in the order you want, you can quickly find the function that interests you and proceed to inspect it.
Using these Android Profiler tools, you can quickly profile the CPU usage of your application and find problem areas in your code for further optimization.
Conclusion
Knowledge of CPU profiling is an essential skill for a software developer since optimizing an application is one of the main parts of its development. With the help of Android Profiler, you can identify cases of misuse of system resources by the application and performance bottlenecks to optimize or eliminate them altogether. However, CPU profiling is only one of the analysis types. In future topics, we will cover memory, network, and energy profiling.