Lab 10 — Perfetto
Record a system trace on a device, open it in the Perfetto UI, and learn to read tracks, slices, thread states and CPU scheduling without drowning in the timeline.
Everything before this lab was a snapshot. A trace is a recording: what ran, on which thread, on which core, for how long, and what it was waiting for. This lab records one and teaches you to read it.
TL;DR#
- A trace is recorded on-device by
perfettoand analysed in the Perfetto UI (ui.perfetto.dev) or withtrace_processor. - Track groups are per-process: a process track contains its threads, and each thread has a track of slices.
- A slice is a span of time on one thread. Its width is duration; its position is when.
- Thread state shows why a thread was not running: running, runnable, sleeping, blocked.
- CPU tracks show which thread occupied which core, which is how you tell a slow algorithm from a starved one.
Mental model#
Think of a hospital's monitoring system.
Each patient (process) has a chart with several traces (threads): heart rate, breathing, medication events. The charts share a time axis, so you can see that a medication event on one chart coincides with a change on another.
The slice is a recorded event with a start and an end. The thread state is the line between events: what the patient was doing while nothing was recorded. And the CPU track is the ward's staffing chart, showing who was actually being attended to.
Where the analogy breaks#
A hospital chart is written by staff who know what matters. A trace records everything that was instrumented, whether or not it matters, and the instrumentation itself has a cost. That is why traces are short and why "record everything" is not a strategy.
The shape of a trace#
Before recording one, it helps to know what you are looking at. The diagram below is an annotated model of a cold start, drawn as a Perfetto-style timeline. It is not a captured trace: the axis is ordinal, so it shows which slices start before which, not measured durations.
Text description of this diagram
An annotated cold start drawn as five rows. The top row is the app main thread with slices for bindApplication, activityCreate, inflate, first draw. The second row is the RenderThread with syncFrame and dequeueBuffer. The third row is SurfaceFlinger with composite and present. The bottom two rows are CPU scheduling lanes: the app process on CPU 0 and SurfaceFlinger on CPU 4. Four numbered annotations point at the moments that matter: the first app code after the fork, the gaps on the main thread, the handover to the RenderThread, and the presentation that ends time to initial display.
The four annotations are the reading method in miniature. Each one points at a different kind of evidence: the start of app work, a gap that means waiting, a thread handover, and the moment the frame reaches the display. Learn to find those four things and most traces become readable.
Deep dive#
Recording#
The device-side recorder is perfetto. A trace config is a protobuf text file describing buffers
and data sources. The shape is always the same: a buffer, then one or more data sources, then a
duration.
buffers {
size_kb: 32768
fill_policy: RING_BUFFER
}
data_sources {
config {
name: "linux.ftrace"
target_buffer: 0
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_wakeup"
atrace_categories: "am"
atrace_categories: "gfx"
atrace_categories: "view"
atrace_categories: "binder_driver"
}
}
}
duration_ms: 10000The atrace_categories are the important part for app work: am for activity management, gfx
for graphics, view for view-system events, binder_driver for Binder transactions. The
sched_switch event is what makes the CPU tracks and thread states possible.1
Record it by piping the config into the device:
cat trace.cfg | adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/trace.pftrace
adb pull /data/misc/perfetto-traces/trace.pftraceThe -c - reads the config from stdin, --txt says the config is text protobuf, and -o names the
output. The pull is a normal adb pull.
Opening it#
The Perfetto UI at ui.perfetto.dev opens a trace locally in the browser; the file is not uploaded.
That is the fastest path and the one to use for a first trace.
You can also analyse it with trace_processor, which is a SQL engine over the trace:
trace_processor query trace.pftrace "SELECT ts, dur, name FROM slice LIMIT 10"The query subcommand loads the trace, runs the SQL and prints the result.2
That is the subject of lab 14.
Reading the timeline#
The UI has four kinds of row, and each answers a different question.
Process tracks. One row group per process, containing its threads. Use it to find your app.
Thread tracks. One row per thread, with slices. The main thread and the RenderThread are the two you will look at most.
Thread state tracks. The thin row under a thread showing running, runnable, sleeping and blocked intervals. A gap in the slice row with a "runnable" thread state means the thread wanted to run but had no CPU; a "sleeping" state means it was waiting for something else.
CPU tracks. One row per core, showing which thread ran when. This is how you answer "was it slow because it was doing work, or because it was not scheduled?".
Slices and nesting#
Slices can nest: a slice named activityCreate can contain inflate, which contains inflateItem
for each view. The nesting is the call structure, and it is how you attribute time.
Two operations are worth knowing in the UI:
- Expand a slice group to see its children.
- Select a slice to see its arguments, its duration and its thread.
The detail panel is where the trace stops being a picture and becomes evidence: it shows the exact
ts and dur, the arguments the instrumentation recorded, and often the source location.
What is not in the trace#
A trace only contains what was instrumented. Kernel scheduling and ftrace events are nearly always available; app-level slices only appear if the app or the framework emitted them. That is why a release build has fewer slices than a debuggable one, and why "there is no slice for it" does not mean "it did not happen".
Walkthrough#
- Confirm the device supports perfetto
adb shell perfetto --helpYou should seeA usage message or help output. - Record a ten second trace
Write the config to a file, then
cat trace.cfg | adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/trace.pftraceYou should seeA command that blocks for ten seconds, then returns. - Pull the trace
adb pull /data/misc/perfetto-traces/trace.pftraceYou should seeA file size reported by adb. - Open it in the UI
Open
https://ui.perfetto.devand load the file. The trace is processed locally.You should seeA timeline with process, thread and CPU tracks. - Find your app
Type the package name in the UI search box.
You should seeA process track named after the package.
adb shell perfetto --help
cat trace.cfg | adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/trace.pftrace
adb pull /data/misc/perfetto-traces/trace.pftraceExample output
Illustrative — requires a device and a config file.Challenges#
Challenge 1 — Distinguish running from runnable#
Find a thread that was runnable but not running, and explain what that means.
Reveal the answer
Look at the thread state track under a busy thread. A "runnable" interval is time the thread was ready to execute but no CPU was available. It is a scheduling problem, not an algorithm problem. The fix is usually to reduce the number of runnable threads competing for the same cores, or to move work to a core that is free, not to optimise the code on that thread.
Challenge 2 — Find the widest slice on the main thread#
Identify the longest single slice in your app's main thread and say what it was.
Reveal the answer
In the UI, select the main thread's track and use the slice list or click through the widest bar. The detail panel shows the name and duration. In SQL, the same question is:
SELECT name, dur FROM slice
WHERE dur > 0
ORDER BY dur DESC
LIMIT 10;The widest slice is usually either the real work or a wait that was instrumented as one slice.
Challenge 3 — Prove a wait with a Binder slice#
Find a Binder transaction slice on the main thread and identify what it was waiting for.
Reveal the answer
Filter the trace for binder slices, or look at the binder_driver category data. A synchronous
Binder call appears as a slice on the caller's thread whose duration is the round trip. If the
caller is blocked and the server thread is also visible, you can see the server side and measure
the time spent in the service versus the time spent in transit.
What you learned#
You can record a trace, open it, and read the four kinds of track. That is the foundation for the next three labs, which ask specific questions of a trace.
Next#
Lab 11 — App startup uses a trace to break a cold start into phases.
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsAndroid tracing with Perfettoperfetto.devaccessed 2026-10-27↩
- docsTrace Processorperfetto.devaccessed 2026-10-27↩
- docsPerfetto documentationperfetto.devaccessed 2026-10-27↩
- docsOverview of system tracingdeveloper.android.comaccessed 2026-10-27↩
- docsPerformance overviewdeveloper.android.comaccessed 2026-10-27↩