CN

Android Perfetto Series 13: Perfetto SDK, Track Event, and App Field Tracing

Word count: 7.3kReading time: 45 min
2026/05/04
loading

A system trace can tell you when a thread ran, which CPU core it ran on, and which FrameTimeline frame missed its deadline. With Binder/ftrace/sched evidence enabled, it can also reconstruct Binder calls and clues about waiting. But it does not know which frame ID a player is decoding, which scene a game engine is loading, or how long an application task has spent in a queue.

Track Events can record decoding, texture uploads, queue depth, and cross-thread task IDs. Put them in the same window as the system tracks to find a candidate application phase, then use RenderThread, GPU, and FrameTimeline evidence to check whether it affected a visible frame.

The article follows the practical integration order: decide whether instrumentation is needed and at which layer, define an application-phase dictionary and backend, then build the minimal integration, category controls, combined system capture, and cross-thread flows. Finally, control write volume and define the app-side field tracing protocol.

Version note (reviewed October 2026): C++ SDK behavior, category precedence, and triggers are checked against Perfetto v58.3. AndroidX tracing-perfetto responsibilities refer to 1.0.0. Record the device tracing-service, app SDK, and analysis-side Trace Processor versions separately. Example settings are laboratory starting points, not support guarantees for every Android device or production environment.

Perfetto Series Catalog

  1. Android Perfetto Series Catalog
  2. Android Perfetto Series 1: Introduction to Perfetto
  3. Android Perfetto Series 2: Capturing Perfetto Traces
  4. Android Perfetto Series 3: Familiarizing with the Perfetto View
  5. Android Perfetto Series 4: Opening Large Traces via Command Line
  6. Android Perfetto Series 5: Choreographer-based Rendering Flow
  7. Android Perfetto Series 6: Why 120Hz? Advantages and Challenges
  8. Android Perfetto Series 7: MainThread and RenderThread Deep Dive
  9. Android Perfetto Series 8: Understanding Vsync and Performance Analysis
  10. Android Perfetto Series 9: Interpreting CPU Information
  11. Android Perfetto Series 10: Binder Scheduling and Lock Contention
  12. Android Perfetto Series 11: PerfettoSQL, Trace Processor and Regression Detection
  13. Android Perfetto Series 12: Trace Dataflow and Data Loss
  14. Android Perfetto Series 13: Perfetto SDK, Track Event and App Field Traces
  15. Android Perfetto Series 14: heapprofd and Memory Profiling
  16. Android Perfetto Series 15: Boot Traces and Long-running Field Tracing
  17. Android Perfetto Series 16: GPU, Power Counters and Hardware Bottlenecks
  18. Android Perfetto Series 17: Scenario Automation and Platform Tracing
  19. Android Perfetto Series 18: Input Response Latency
  20. Video (Bilibili) - Android Perfetto Basics and Case Studies
  21. Video (Bilibili) - Android Perfetto: Trace Graph Types - AOSP, WebView, Flutter + OEM System Optimization

When Application Semantics Are Needed

If there is no gap in application semantics, you do not need to integrate the SDK. Choose the tool for the scenario first:

Scenario Recommendation
Temporarily marking function duration in Java/Kotlin Use android.os.Trace or AndroidX tracing, and enable app atrace during capture
Long-term instrumentation in Android native / C++ modules Use Track Event from the Perfetto SDK
Cross-platform engines, games, players, and Camera pipelines Use Track Event with stable arguments and counters
Complex structured state that ordinary slices/counters cannot express Consider a custom DataSource
Only system thread states, Binder, or frequencies are needed Start with system data sources; no SDK is needed without an application-semantics gap

Application and system events in the same observation window

Diagram: a system-backend session places app events and independent system data sources in the same trace.

For Android-only use cases, the official guidance also recommends continuing to use android.os.Trace and NDK ATrace_* when they are sufficient. These events enter Perfetto through the atrace path and suit ordinary app function markers. During capture, configure atrace_apps: "com.example.app" in linux.ftrace, or use -a com.example.app on the command line to enable the app. The SDK still suits Android native, cross-platform, and structured application-semantics scenarios; simple Android-only sections do not need to replace ATrace just for consistency.

The Perfetto SDK is better suited to native and cross-platform modules. It produces Track Events directly and supports categories, slices, counters, flows, arguments, and independent tracks, making it convenient to place them on the same timeline as system traces. This article uses the C++ SDK. The upstream repository also contains Java interface sources and a Rust SDK, so “official support is C/C++ only” is no longer a valid selection premise. In v58.3 src/android_sdk, however, PerfettoTrace is marked @hide and depends on JNI and platform-related annotations. Source availability does not establish a stable public Java API or a ready-to-use app dependency. Java/Kotlin projects must verify distribution, native linkage, and compatibility contracts. Simple sections can continue using android.os.Trace; evaluate supported wrappers when SDK features are needed.

Choose app-side tracing capabilities by their boundaries:

Capability Typical use Version and field constraints
android.os.Trace / AndroidX Trace Coarse Java/Kotlin sections Synchronous sections start at API 18; check release-app capture eligibility against OS version, debuggable/profileable settings, and the capture tool; enable atrace_apps
NDK ATrace_* Coarse native sections Synchronous sections start at API 23, async/counter APIs at API 29; guard API availability on older devices
AndroidX tracing-perfetto Writing low-overhead SDK events, enabled with external capture tooling 1.0.0 supports preconfigured startup initialization; this does not grant the app permission to read system traces
Perfetto SDK kSystemBackend Writing Track Events from native / cross-platform modules into system traces Android integrates traced from Android 9; verify enablement, producer-socket access, and consumer permissions on the actual device

ATrace also has overhead. Adding android.os.Trace / ATrace_* to hot loops, every lock attempt, or every object lets JNI, trace_marker, and the kernel path themselves perturb the result. Java/Kotlin and NDK ATrace are better suited to coarse sections. For high-frequency application state, prefer SDK category gating, sampling, or debug categories that are disabled by default.

SDK Integration Boundaries

The Perfetto SDK offers two layers of capability:

Capability Suitable for expressing Cost
Track Event Function durations, application phases, queue lengths, frame IDs, request IDs, and cross-thread relationships Low integration cost; native support in Trace Processor and the UI
Custom DataSource Custom protobuf state, high-frequency structured data, and specialized binary formats Requires schema maintenance and usually additional Trace Processor parsing

Prefer Track Event whenever slices, counters, flows, and arguments can express the problem. A custom DataSource has a higher entry cost and is not a good first instrumentation design.

A custom DataSource suits two situations: ordinary slices, counters, and debug annotations cannot express the structure, such as a periodic subsystem state dump; or the data volume requires a strongly typed schema to reduce the size of each event.

Its cost is also direct: Trace Processor does not understand your protobuf by default, so you must add parsing, query tables, or Trace Summary output. Starting with Track Event avoids maintaining an entire additional data format.

Define the Application-Phase Dictionary Before Writing Code

Track Event is not about placing a macro in every function. I would define the application-phase dictionary before writing the instrumentation: event names, phases, field units, and the fallback when a field is missing. The SQL and reports depend on those choices.

event_name category phase Required arguments Optional arguments Units / identity scope Sampling Report metrics
DecodeFrame player decode frame_id, stream_id, codec width, height frame_id is unique within stream_id Once per frame; enable debug fields as needed decode_dur_ms, decode_max_ms
UploadTexture rendering upload frame_id, texture_bytes surface_id texture_bytes is in bytes At most once per frame upload_dur_ms, upload_bytes
RenderSubmit rendering submit frame_id, surface_id queue_depth queue_depth is in items Once per frame submit_dur_ms, queue_depth_items
WaitForBuffer camera wait request_id, buffer_queue producer request_id is unique within a session Record only when the wait exceeds a threshold wait_for_buffer_ms

Renaming or deleting fields, or changing units, requires incrementing arg_schema_version. Long-term report fields should be called frame_id / request_id from the application schema, rather than debug.frame_id; also record frame_id_domain, nullable_policy, and compatibility policy. When RenderSubmit is missing, the report can describe only decode/upload phases and must withhold end-to-end conclusions. Set missing_marker=RenderSubmit and evidence_grade=partial.

The three-phase SQL below additionally requires consistently typed session_id/stream_id/frame_id on every phase. For thresholded WaitForBuffer events, retain the actual start/end timestamps and emit them using the correct clock domain. Opening a scope only after the wait finishes measures instrumentation time, not the wait.

Two Backends Solve Different Problems

The Perfetto SDK can use either an in-process backend or the system backend:

Backend Who controls the trace Suitable scenarios
kInProcessBackend The app creates its own tracing session Single-process validation, offline testing, and application events alone
kSystemBackend An external perfetto command or system service Combined analysis with sched, freq, Binder, and FrameTimeline

The system backend is more common in Android performance analysis. Here the app acts only as a producer, while an authorized external consumer controls the system session. Producer-socket access does not authorize reading other processes’ trace data. The SDK also has consumer APIs; permission to use those with the system backend depends on platform policy.

The combined examples below assume availability of the Android 12+ FrameTimeline data source, subject to device support. The SDK system backend predates Android 12. Production integration still needs package-size, initialization-cost, and enablement-policy evaluation.

Minimal Integration Skeleton

The minimal integration addresses three things: stable categories, the system backend, and queryable event names. A native module can use this skeleton.

1
2
3
4
5
6
7
8
9
10
11
12
13
// tracing_categories.h
#pragma once

#include <perfetto.h>

PERFETTO_DEFINE_CATEGORIES(
perfetto::Category("rendering")
.SetDescription("Rendering pipeline"),
perfetto::Category("player")
.SetDescription("Media pipeline"),
perfetto::Category("pipeline.debug")
.SetDescription("Verbose pipeline events")
.SetTags("debug"));

The header only declares categories. Category names enter traces and subsequent SQL, so keep them stable; do not embed dynamic IDs, page-instance IDs, or request numbers in them.

1
2
3
4
// tracing_categories.cc
#include "tracing_categories.h"

PERFETTO_TRACK_EVENT_STATIC_STORAGE();

PERFETTO_TRACK_EVENT_STATIC_STORAGE() provides the static storage required for Track Event registration. Place it in only one .cc file to avoid multiple definitions.

1
2
3
4
5
6
7
8
9
10
11
12
13
// tracing_init.cc
#include "tracing_categories.h"
#include <mutex>

void InitPerfetto() {
static std::once_flag once;
std::call_once(once, [] {
perfetto::TracingInitArgs args;
args.backends |= perfetto::kSystemBackend;
perfetto::Tracing::Initialize(args);
perfetto::TrackEvent::Register();
});
}

Initialization selects the backend and registers the categories defined above with Track Event. Android system tracing usually uses kSystemBackend, allowing app events to appear in the same trace as system sched, Binder, and FrameTimeline data.

You can then instrument application code. This example uses a slice for duration and an argument for the frame ID:

1
2
3
4
void DecodeFrame(int frame_id) {
TRACE_EVENT("player", "DecodeFrame", "frame_id", frame_id);
DecodeFrameImpl(frame_id);
}

TRACE_EVENT is scoped: it begins on entering the current scope and ends on leaving it. It suits synchronous work such as decode, layout, upload, and submit.

Use TRACE_EVENT_BEGIN and TRACE_EVENT_END for events that do not follow function scope. They help with phases spanning multiple functions, but do not scatter begin/end calls across unrelated call paths: that can confuse same-thread slice nesting, mismatch endings, or make SQL interpretation difficult.

SDK integration also has a build boundary. The official C++ SDK is a C++17 library, commonly distributed as the amalgamated perfetto.h and perfetto.cc source files for integration into existing native build systems.

Adding perfetto.h alone is insufficient. Check the language standard, threading library, symbol stripping, package-size increase, and whether Tracing::Initialize() has completed early in app-process startup.

Use perfetto.h and perfetto.cc from the same SDK release and compile/link the latter into the target. Do not mix header and implementation versions. The std::call_once above protects this module’s entry point only; other libraries in the process also need a coordinated initialization policy.

With ordinary initialization, Track Events begin recording when the data source is enabled. v58.3 separately offers the explicit SetupStartupTracingBlocking API; ordinary Initialize does not enable that behavior by default. AndroidX tracing-perfetto 1.0.0 can initialize instrumentation at app startup after external tooling configures it. It does not independently capture zygote, ActivityTaskManager, or first-frame presentation; cold-start analysis still needs the system data sources.

Decide initialization timing and process boundaries together. Call Tracing::Initialize() only once per process, on a deterministic process-entry path such as JNI_OnLoad or a shared-library load triggered by Application#onCreate, with idempotence protection. Track Events emitted before registration are silently discarded without errors, making them difficult to distinguish from actual packet loss later. Every process in a multiprocess app needs its own Initialize + Register; subprocesses such as :remote and :player do not inherit the main process’s initialization. With the system backend, starting a process after the tracing session has begun is fine: once its producer connects to traced, matching data sources are enabled automatically. Events before that connection simply do not exist; do not interpret that gap as application idleness.

Categories Are Production Controls

Categories determine which events are enabled. Design them as a configuration interface rather than inventing names casually. In the C++ SDK (perfetto::Category), categories without debug or slow tags are enabled by default when no rule matches. The C SDK (PerfettoTeCategory) behaves oppositely: unmatched categories are disabled by default.

The main rule for production presets is simple: explicitly set disabled_categories: "*", then allowlist the required categories. This avoids C/C++ default differences and accidental enablement of debug categories in the field. The debug and slow tags are disabled by default; enable them explicitly for targeted analysis.

Rules have precedence: an exact enabled category wins over a disabled tag, including debug. The allowlist controls event enablement; it is not an unconditional security boundary.

Common divisions are:

  • player: major player phases such as prepare, decode, and render.
  • rendering: render submission, texture upload, and scene transitions.
  • camera: requests, HAL callbacks, and buffer queues.
  • pipeline.debug: high-frequency debugging events; add the debug tag when defining the category.
  • pipeline.slow: events with higher overhead; add the slow tag when defining the category.

Design categories around subsystems and enablement granularity, not pages, experiment groups, or request types. Put those details in arguments or metadata to avoid an explosion in category count.

This configuration selects categories explicitly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
buffers {
size_kb: 65536
fill_policy: RING_BUFFER
}

data_sources {
config {
name: "track_event"
target_buffer: 0
track_event_config {
disabled_categories: "*"
enabled_categories: "player"
enabled_categories: "rendering"
}
}
producer_name_filter: "com.example.app"
}

The benefit is that field captures can change which application events are enabled through configuration alone, without replacing the app build. With the system backend, track_event may come from multiple producers. Field or production presets should constrain scope using producer_name_filter or a regex. The example’s com.example.app is a placeholder: confirm the actual producer name from the trace, data-source list, or producer list first. Use multiple exact filters or regex filters for multiprocess apps.

Capture Alongside System Traces

Application events explain “what the application is doing.” System events explain “why this work is slow.” A combined capture usually places track_event and ftrace in the same configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
buffers { size_kb: 16384 fill_policy: RING_BUFFER } # 0: sched / ftrace
buffers { size_kb: 4096 fill_policy: RING_BUFFER } # 1: app track event

data_sources {
config {
name: "linux.ftrace"
target_buffer: 0
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
ftrace_events: "power/cpu_frequency"
ftrace_events: "power/cpu_idle"
atrace_categories: "gfx"
atrace_categories: "view"
atrace_categories: "binder_driver"
atrace_apps: "com.example.app"
}
}
}

data_sources {
config {
name: "android.surfaceflinger.frametimeline"
target_buffer: 0
}
}

data_sources {
config {
name: "track_event"
target_buffer: 1
track_event_config {
disabled_categories: "*"
enabled_categories: "player"
enabled_categories: "rendering"
}
}
producer_name_filter: "com.example.app"
}

duration_ms: 10000

These central buffers total 20 MiB. The original 160 MiB exceeded the default 128 MiB total limit when v58.3 extra guardrails are enabled. Twenty MiB does not guarantee ten seconds of retention; tune against write rates and stats, budgeting per-CPU ftrace buffers separately. Save this complete config as app-sdk.pbtxt, replace the package and actual producer names, and use an authorized test device:

1
2
3
adb shell perfetto --query
adb shell perfetto --txt -c - -o /data/misc/perfetto-traces/app-sdk.perfetto-trace < app-sdk.pbtxt
adb pull /data/misc/perfetto-traces/app-sdk.perfetto-trace .

The second command runs in the foreground for ten seconds; exercise the app during capture and pull only after successful completion. --query lists currently registered sources/producers, so a process that has not started may be absent. Missing sources, denied permissions, or a stale file at the same path must not be mistaken for a successful capture.

After recording, confirm with SQL that the events arrived. This query uses the thread_slice view introduced in Part 11 to retrieve process names, thread names, and application arguments directly. thread_slice, provided by slices.with_context, already joins slice, track, thread, and process context. When reading this article independently, keep the preceding INCLUDE.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INCLUDE PERFETTO MODULE slices.with_context;

SELECT
id AS slice_id, upid, utid, ts AS ts_ns, dur AS dur_ns,
ROUND(ts / 1e6, 3) AS ts_ms,
CASE WHEN dur >= 0 THEN ROUND(dur / 1e6, 3) END AS dur_ms,
name,
thread_name,
process_name,
EXTRACT_ARG(arg_set_id, 'debug.frame_id') AS frame_id
FROM thread_slice
WHERE process_name = 'com.example.app'
AND name IN ('DecodeFrame', 'RenderScene', 'UploadTexture')
ORDER BY ts;

This query suits Track Events or atrace slices on default thread tracks. Events on independent tracks or process tracks may lack a utid and therefore may not appear in thread_slice. Use process_slice, thread_or_process_slice, or query slice JOIN track directly and reconstruct context from track.name/parent_id.

The query does more than confirm event presence: its results can be correlated by time range with thread_state, CPU frequency, Binder events, and FrameTimeline. A TrackEvent slice is evidence on the application timeline; it does not mean the CPU was executing that work continuously. CPU execution requires additional correlation with sched / thread_state, distinguishing the default ThreadTrack, custom Track, and cross-thread flows. Ordinary key/value arguments enter args as debug annotations, read in SQL as debug.<key>. Typed TrackEvent fields do not use this debug.* argument representation; they require corresponding parsing, tables, or dedicated fields.

Time-range correlation is only the first step. An app’s frame_id identifies an application frame; FrameTimeline’s frame/vsync identifies a system frame. Temporal overlap alone can assign queuing, prerendering, or cross-thread work to the wrong frame. Confirm against Choreographer frame/vsync, JankStats frame start, RenderThread/HWUI slices, and actual_frame_timeline_slice / expected_frame_timeline_slice.

The frame_id here is a debug annotation, suitable for investigation and ad hoc SQL. Debug annotations are debugging fields by default. If they feed long-term metrics, freeze their names, types, units, defaults, and migration policies as an interface. Consider typed TrackEvent fields or custom DataSources only when you control Perfetto proto definitions, parsing, and release procedures. Field presets can filter some content with filter_debug_annotations/filter_dynamic_event_names, but this is not complete redaction. The former also removes IDs required by this SQL, so reports must retain missing_identity.

Application-phase reports must check all three required markers using the composite process/session/stream/frame key; seeing RenderSubmit alone does not make the record complete. The following query requires all three phases to include session_id/stream_id/frame_id. The earlier minimal single-marker example includes only frame_id, so add the remaining arguments first. Keep events lacking identity fields separate as missing_identity; do not combine them with other null-ID events.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
INCLUDE PERFETTO MODULE slices.with_context;

WITH app_events AS (
SELECT
id, upid, thread_name, name AS phase, ts, dur,
EXTRACT_ARG(arg_set_id, 'debug.session_id') AS session_id,
EXTRACT_ARG(arg_set_id, 'debug.stream_id') AS stream_id,
EXTRACT_ARG(arg_set_id, 'debug.frame_id') AS frame_id
FROM thread_slice
WHERE process_name = 'com.example.app'
AND name IN ('DecodeFrame', 'UploadTexture', 'RenderSubmit')
),
frame_quality AS (
SELECT
upid, session_id, stream_id, frame_id,
CASE
WHEN SUM(phase = 'DecodeFrame') = 1
AND SUM(phase = 'UploadTexture') = 1
AND SUM(phase = 'RenderSubmit') = 1
AND MIN(dur) >= 0 THEN 'complete'
ELSE 'partial_or_duplicate'
END AS evidence_grade
FROM app_events
WHERE session_id IS NOT NULL AND stream_id IS NOT NULL AND frame_id IS NOT NULL
GROUP BY upid, session_id, stream_id, frame_id
)
SELECT
e.upid, e.session_id, e.stream_id, e.frame_id, e.phase,
e.id AS slice_id, e.ts AS start_ns, e.dur AS raw_dur_ns,
ROUND(e.ts / 1e6, 3) AS start_ms,
CASE WHEN e.dur >= 0 THEN ROUND(e.dur / 1e6, 3) END AS dur_ms,
e.thread_name,
COALESCE(q.evidence_grade, 'missing_identity') AS evidence_grade
FROM app_events e
LEFT JOIN frame_quality q USING (upid, session_id, stream_id, frame_id)
ORDER BY e.upid, e.session_id, e.stream_id, e.frame_id, e.ts;

Here, complete means only that the number and closure state of the application markers satisfy this example’s contract. It does not prove that the frame was displayed. The following report illustration includes additional FrameTimeline correlation and expansion of missing items; it is not the direct output of the SQL above:

It also does not validate stage ordering, sampling coverage, or schema-version compatibility. A frame with no markers produces no “missing frame” row. Missing-frame rates or end-to-end coverage need an independent expected-frame inventory and identity mapping.

1
2
3
4
trace_name,scenario,frame_or_request_id,phase,start_ms,dur_ms,thread_name,frame_jank_type,evidence_grade,missing_marker
run01.perfetto-trace,feed_scroll,1082,DecodeFrame,1240.2,5.8,decoder-1,jank,partial_or_duplicate,
run01.perfetto-trace,feed_scroll,1082,UploadTexture,1248.7,9.1,RenderThread,jank,partial_or_duplicate,
run01.perfetto-trace,feed_scroll,1082,RenderSubmit,,,,jank,partial,RenderSubmit

Use Flows or Independent Tracks for Cross-Thread Tasks

Many application delays occur between queues rather than inside a single function. For example, a UI thread submits a request, a decoder thread processes it, and a render thread consumes it. Same-thread slices alone make it hard to recognize that they belong to the same frame.

Flows can connect related events. Allocate a token before enqueueing and pass the same FrameToken with the task to later threads. Do not allocate again at each stage or treat a reusable frame_id or hash as unique. These wrappers show instrumentation; the actual queue transports the token, and each callable encloses real enqueue, decode, or consume work. Use one allocator across the process, not one per shared library, and prevent wraparound/reuse:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <atomic>
#include <cstdint>
#include "tracing_categories.h"

struct FrameToken {
uint64_t flow_id;
uint64_t session_id;
uint64_t stream_id;
uint64_t frame_id;
};

FrameToken AllocateFrameToken(uint64_t session, uint64_t stream, uint64_t frame) {
static std::atomic<uint64_t> next{1};
return {next.fetch_add(1, std::memory_order_relaxed), session, stream, frame};
}

template <typename Work>
void EnqueueFrame(const FrameToken& t, Work&& enqueue) {
TRACE_EVENT("player", "EnqueueFrame", perfetto::Flow::ProcessScoped(t.flow_id),
"session_id", t.session_id, "stream_id", t.stream_id,
"frame_id", t.frame_id);
enqueue();
}

template <typename Work>
void DecodeFrame(const FrameToken& t, Work&& decode) {
TRACE_EVENT("player", "DecodeFrame", perfetto::Flow::ProcessScoped(t.flow_id),
"session_id", t.session_id, "stream_id", t.stream_id,
"frame_id", t.frame_id);
decode();
}

template <typename Work>
void RenderFrame(const FrameToken& t, Work&& consume) {
TRACE_EVENT("player", "RenderFrame",
perfetto::TerminatingFlow::ProcessScoped(t.flow_id),
"session_id", t.session_id, "stream_id", t.stream_id,
"frame_id", t.frame_id);
consume();
}

When you select an event, the Perfetto UI shows the relationships as arrows. Continue using Flow for intermediate stages and use TerminatingFlow at the final consumption stage.

This example uses ProcessScoped(flow_id), so the ID must remain stable at least within the process over a trace’s lifetime. Do not directly use a temporary pointer that can be reused as a flow ID. For cross-process flows, do not retain ProcessScoped semantics: at minimum, use a globally unique ID and specify its namespace and source process. A multistage pipeline can also allocate a separate flow ID for each edge, such as enqueue_to_decode_flow_id and decode_to_render_flow_id, to avoid reusing a flow across multiple paths.

A long-lived task spanning functions and threads can also use an independent track:

1
2
3
4
5
6
7
8
9
10
11
12
13
void StartRequest(uint64_t request_id) {
perfetto::Track track(request_id);
auto desc = track.Serialize();
desc.set_name("player.request");
perfetto::TrackEvent::SetTrackDescriptor(track, desc);

TRACE_EVENT_BEGIN("player", "Request", track,
"request_id", request_id);
}

void FinishRequest(uint64_t request_id) {
TRACE_EVENT_END("player", perfetto::Track(request_id));
}

These events suit task queues, player pipelines, asynchronous loading, and cross-thread GPU submission. They explain an application’s execution path better than instrumenting every function.

Begin/end still follow stack nesting on a track. Concurrent, non-nested tasks need separate tracks, and cancellation/error paths must also close their slices. In v58.3, Track(id) defaults to the process track as parent and derives its UUID from it; id=0 conflicts with that parent. Dedicated IDs must remain unique in their namespace.

Once a high-cardinality task has ended and will emit no more events, TrackEvent::EraseTrackDescriptor(track) can remove its custom SDK descriptor to limit registry growth. This does not delete historical events already written to the trace.

The ID in perfetto::Track(request_id) must uniquely identify that concurrent task within a trace. Production request IDs and frame IDs are easily reused; reuse merges different tasks onto one track, making them look like one long task or an incorrectly serialized sequence. Add a session/pipeline namespace or generate a dedicated 64-bit track ID. For ongoing analysis, include a track descriptor/name so the UI and SQL can identify its lifecycle. If descriptors or interned data are lost, return to the stats checks in Part 12.

Counters Represent State, Not Logs

Counters suit numeric values that change over time, such as queue length, in-flight request count, and buffer occupancy. A frame ID is an identifier, not an aggregatable metric; it belongs in slice arguments or flow IDs. This example records decode queue depth:

1
2
3
4
5
void ReportDecodeQueueDepth(int depth) {
TRACE_COUNTER("player",
perfetto::CounterTrack("DecodeQueueDepth", "items"),
depth);
}

Counters should not carry log text, nor should identifiers be averaged or maximized. Keep logs in logcat; retain only numeric values in the trace that can be correlated with the timeline and interpreted as state.

Fix counter units and aggregation semantics. The actual track name above is DecodeQueueDepth; items is unit_name, not an automatically appended suffix. Weight a queue-depth window mean by how long each state holds, not by sample count. Keep the interval before the first known state unknown. Inspect InFlightRequests peaks/durations and BufferBytes peaks/growth/recovery; do not aggregate identifiers.

Event Names Are a SQL Interface

Once event names enter SQL, dashboards, and regression reports, they become an interface. Do not concatenate dynamic IDs into event names.

1
2
3
4
5
// Avoid: each frame gets a different event name, making stable SQL aggregation difficult.
TRACE_EVENT("player", perfetto::DynamicString{"DecodeFrame_123"});

// Prefer: keep the event name stable and put dynamic information in arguments.
TRACE_EVENT("player", "DecodeFrame", "frame_id", frame_id);

A better naming pattern is “phase + action”:

  • DecodeFrame: decode an application frame, with frame_id, stream_id, and codec arguments.
  • UploadTexture: upload a texture or buffer, with frame_id and texture_bytes arguments.
  • RenderScene: submit a scene-rendering pass, with scene_id or surface_id arguments.
  • SubmitRequest: submit a cross-thread request, with request_id and queue_depth arguments.
  • WaitForBuffer: wait for an upstream buffer, correlatable with Binder, thread-state, and buffer-queue evidence.

Stable event names make downstream SQL stable. Convert duration columns consistently with / 1e6: time_to_ms() from time.conversion performs integer division, truncating the maximum duration to whole milliseconds and giving it a different precision from the mean:

Group by upid/utid to separate process restarts and same-named threads. This aggregates closed matching slices over the whole trace, not one operation or the population of frames. A scenario window requires explicit time/identity filtering and separate reporting of unclosed records.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INCLUDE PERFETTO MODULE slices.with_context;

SELECT
upid, process_name,
utid, thread_name,
name,
COUNT(*) AS count,
MAX(dur) AS max_ns,
ROUND(AVG(dur) / 1e6, 1) AS avg_ms,
ROUND(MAX(dur) / 1e6, 1) AS max_ms
FROM thread_slice
WHERE process_name = 'com.example.app'
AND name IN ('DecodeFrame', 'UploadTexture', 'RenderScene')
AND dur >= 0
GROUP BY upid, process_name, utid, thread_name, name
ORDER BY max_ns DESC;

Control Write Volume

Track Event has low overhead, but it is not free. Conventional ATrace traffic pressures ftrace per-CPU buffers, while SDK Track Events mainly pressure producer shared memory, central buffers, and incremental state. v58.3 also offers routing options such as atrace_categories_prefer_sdk; identify the actual data source and stats rather than inferring the path solely from the API name.

Set a budget for each instrumentation class:

  • Event density: estimate write volume from events/sec, events per frame, argument bytes, and target trace duration. At 120 Hz, multiple threads, stages, and dynamic strings can make even once-per-frame recording too dense.
  • Data shape: do not enable events for every small object, every lock attempt, or every pixel-level loop by default. Keep high-frequency debug categories disabled; targeted traces can use 1/N sampling or a short window after a trigger.
  • Argument cost: pass inexpensive scalars directly; put expensive strings, JSON, container iteration, and state snapshots taken under locks inside an enabled check or lambda. Category gating cannot eliminate argument-construction work already performed before the macro call.
  • Producer-side fallback: evaluate shmem_size_hint_kb and BufferExhaustedPolicy::kStall only after confirming shared-memory bursts as the bottleneck. Part 12 covers their tradeoffs. First reduce event density and split large packets rather than immediately tuning parameters.
  • Capture acceptance: place application Track Events in a separate central buffer for long traces, then run the acceptance query below over traced_buf_* and track_event_* statistics. Separate buffers isolate central-buffer contention only; they do not fix burst loss in producer shared memory.
  • For ring buffers, watch Track Event descriptors, string interning, and incremental state. Reemit required metadata when incremental state is rebuilt on the same packet sequence and reduce contention in its buffer. clear_period_ms requests periodic rebuilding from supporting producers; it neither restores lost events nor permits moving dependent metadata arbitrarily to another sequence.

Start the unified acceptance query with Part 12’s health check, then add Track Event-specific items:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SELECT name, idx, severity, source, value
FROM stats
WHERE value != 0
AND (
severity IN ('error', 'data_loss')
OR name IN (
'traced_buf_chunks_overwritten',
'traced_buf_chunks_discarded',
'traced_buf_trace_writer_packet_loss',
'traced_buf_sequence_packet_loss',
'traced_buf_incremental_sequences_dropped',
'traced_buf_patches_failed',
'track_event_parser_errors',
'track_event_tokenizer_errors',
'track_hierarchy_missing_uuid',
'track_event_thread_invalid_end',
'track_event_missing_sequence_id',
'interned_data_tokenizer_errors',
'tokenizer_skipped_packets',
'packet_skipped_seq_needs_incremental_state_invalid'
)
OR name GLOB 'track_descriptor_*'
OR name GLOB 'track_event_skipped_*'
)
ORDER BY name, idx;

Give the report a separate track_event_trust_level. Even when available packet-loss statistics are zero, investigate nonzero track_event_tokenizer_errors or track_hierarchy_missing_uuid; affected events cannot simply be treated as complete evidence.

Distinguish an available zero-valued statistic from an unavailable statistic or parser failure. An empty result from the nonzero query does not mean every check passed. Locate affected sequences, tracks, and windows before generalizing a local error to the entire trace.

The App-Side Field Tracing Protocol

A common pitfall when integrating Perfetto into an app is treating the app as a small adb shell perfetto client. Ordinary apps do not have permission to start arbitrary system tracing, and dynamically delivering expensive TraceConfigs to production apps is inappropriate. A field solution is better divided into three layers:

  • Platform: provision controlled TraceConfigs that declare triggers, ring buffers, file paths, data-source allowlists, and capture limits.
  • App: record application markers and page state, detect slow frames, hangs, timeouts, and other anomalies, and activate triggers through the platform contract.
  • Server: receive trace packages, run stats, summary SQL, and aggregate indexing, and provide evidence that human analysts can revisit.

Perfetto STOP triggers suit this scenario. A trace records continuously into a ring buffer; after the app detects a problem, it activates a declared name, and the system wraps up after stop_delay_ms. This preserves context before the problem as well as the tail after the trigger.

A controlled field UI-jank preset can look like this. It suits laboratory or field investigations with a rollout allowlist, controlled sampling, measured write rates, and stats-based acceptance. Default production presets should reduce atrace categories, retaining only sched, FrameTimeline, and minimal Track Events if necessary. This is not a comprehensive graphics preset, but it supports initial investigation using the main thread, RenderThread/HWUI, FrameTimeline, CPU scheduling, SurfaceFlinger-side FrameTimeline/thread-scheduling clues, and app Track Events. Switch to a dedicated graphics preset for attribution involving Layers, Transactions, HWC, or Display.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
buffers { size_kb: 16384 fill_policy: RING_BUFFER } # 0: sched / ftrace
buffers { size_kb: 4096 fill_policy: RING_BUFFER } # 1: app track event / frame context

incremental_state_config {
clear_period_ms: 5000
}

trigger_config {
trigger_mode: STOP_TRACING
trigger_timeout_ms: 1800000 # Maximum window for waiting for a trigger
triggers {
name: "app_ui_jank"
stop_delay_ms: 1000
max_per_24_h: 8
skip_probability: 0.8
}
}

data_sources {
config {
name: "linux.ftrace"
target_buffer: 0
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_wakeup"
ftrace_events: "sched/sched_waking"
ftrace_events: "power/cpu_frequency"
ftrace_events: "power/cpu_idle"
atrace_categories: "gfx"
atrace_categories: "view"
atrace_categories: "wm"
atrace_apps: "com.example.app"
}
}
}

data_sources {
config {
name: "android.surfaceflinger.frametimeline"
target_buffer: 1
}
}

data_sources {
config {
name: "track_event"
target_buffer: 1
track_event_config {
disabled_categories: "*"
enabled_categories: "player"
enabled_categories: "rendering"
}
}
producer_name_filter: "com.example.app"
}

Here skip_probability: 0.8 skips 80% of activations before checking the rolling 24-hour limit. For laboratory validation, save a separate preset with skip_probability: 0 and a test trigger whose quota is not exhausted. If no trigger is accepted within 30 minutes, STOP_TRACING ends without returning data. Thirty minutes is the waiting timeout, not guaranteed ring-buffer retention. Products still need platform authorization, sampling, and quota management.

For a laboratory test, save the trigger configuration above separately as app-trigger.pbtxt and start it using the capture command with that filename, then send the trigger from another terminal. The earlier ten-second config declares no trigger and cannot serve this test. STOP ends the session; continued monitoring requires the platform to start another one. After checking laboratory sampling and quota settings, use:

1
adb shell /system/bin/trigger_perfetto app_ui_jank

The Perfetto trigger model is that an authorized consumer declares trigger names in advance, while producers such as apps can only activate declared names. Native modules integrating the Perfetto SDK can also trigger through the SDK. In products, a platform interface should still centralize permissions and quotas:

1
perfetto::Tracing::ActivateTriggers({"app_ui_jank"}, 10000);

The second argument is in milliseconds. The call returns immediately and signals initialized backends connected now or connecting within 10000 ms. It is neither a blocking wait nor the one-second post-trigger tail. Return does not prove acceptance or trace creation. Without producer_name_regex, any connected producer can activate a matching name; trigger names and producer filtering are not business-authorization credentials.

Production apps should not assume they can execute /system/bin/trigger_perfetto. Prefer a controlled platform interface, such as a system service, vendor SDK, enterprise device-management component, or test-framework proxy. The app submits only a constrained request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"preset": "ui_jank_field_v3",
"trigger": "app_ui_jank",
"reason": "three_jank_frames_in_2s",
"jank_frames_is_excerpt": true,
"scenario_id": "feed_scroll_120hz",
"session_id": "s-20260504-001",
"action_id": "scroll-42",
"page": "Feed",
"action": "scroll",
"elapsed_realtime_ns": 123456789000000,
"jank_frames": [
{
"frame_start_ns": 123456700000000,
"clock_domain": "CLOCK_MONOTONIC",
"duration_ns": 32000000,
"expected_duration_ns": 8333333,
"states": {
"page": "Feed",
"action": "scroll"
}
}
]
}

The platform decides whether to trigger based on the preset allowlist, sampling rate, permissions, device state, and daily quota. This interface contract is easier to maintain long term than letting the app assemble its own TraceConfig. Server-side matching should output trigger_event_ts, nearest_marker, marker_delta_ms, and matched_by, so correlating external JSON with the trace timeline does not depend on human guesswork.

Trigger policy should reflect what users perceive rather than internal implementation details:

Trigger Typical signal What to inspect in the trace
Slow UI frames Consecutive slow frames in JankStats or severe budget overruns Main thread, RenderThread, FrameTimeline, CPU
Warning signs of a main-thread hang 2 s / 4 s / 8 s watchdog Main-thread state, Binder, locks, IO, system services
Application timeout Camera open, player prepare, or initial-screen timeout App markers, Binder, HAL, thread states

Keep trigger names stable once they enter platform presets. Renaming them breaks continuity in server aggregation, SQL templates, and historical data.

Trace Packages Need Context

Separate the minimum field package from later platform extensions. This article requires only the essentials: the trace itself, application metadata, capture preset, and stats-based trust results. Upload policy, log allowlists, and retention periods are platform rules that can be designed separately later.

1
2
3
4
5
6
7
8
trace-package/
trace.perfetto-trace
app_metadata.json
app_events.jsonl
trace_preset.txt
trace_stats.json
upload_policy.json # Add when building out the platform
log_excerpt.txt # Add when building out the platform

app_metadata.json describes the incident context. Keep its fields fixed and include a schema version:

All JSON below is illustrative. The earlier jank_frames array shows only one of the claimed three frames; real requests need full evidence or an explicit summary. 8.333333 ms illustrates a fixed 120 Hz period, not necessarily the JankStats threshold or a variable-refresh frame deadline. Keep unknown refresh rates and thermal states null/unknown rather than defaulting to 120/normal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"schema_version": 3,
"app_version": "8.12.0",
"package_name": "com.example.app",
"page": "Feed",
"action": "scroll",
"trigger": "app_ui_jank",
"trigger_reason": "three_jank_frames_in_2s",
"trigger_elapsed_realtime_ns": 123456789000000,
"scenario_id": "feed_scroll_120hz",
"session_id": "s-20260504-001",
"action_id": "scroll-42",
"refresh_rate_hz": 120,
"thermal_state": "normal",
"trace_preset": "ui_jank_field_v3"
}

The other files also need a minimal set of fields:

trace_stats.json should at least preserve Part 12’s health-check results:

Overwritten chunks are not a count of lost events and do not alone prove target-frame damage. Here idx=1 refers to the buffer shared by Track Event and FrameTimeline in this preset. Inspect the target window, slice closure, and parsing errors before deciding metric usability; do not automatically downgrade every app marker.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"trace_quality_status": "needs_scope_check",
"track_event_trust_level": "undetermined",
"stats": [
{
"name": "traced_buf_chunks_overwritten",
"idx": 1,
"idx_semantics": "buffer",
"severity": "info",
"source": "trace",
"value": 12,
"buffer_sources": ["track_event", "android.surfaceflinger.frametimeline"],
"decision": "check_target_window_before_grading"
}
]
}

upload_policy.json specifies whether the field package can be uploaded and how long it is retained:

1
2
3
4
5
6
7
{
"policy_version": 2,
"network": "wifi_only",
"daily_device_quota": 3,
"retention_days": 14,
"encryption": "required"
}

app_events.jsonl stores the trigger and a small amount of app-side state as one JSON event per line, sharing scenario_id/session_id/action_id with Track Events:

1
{"elapsed_realtime_ns":123456789000000,"event":"trigger","scenario_id":"feed_scroll_120hz","session_id":"s-20260504-001","action_id":"scroll-42","page":"Feed","action":"scroll"}

log_excerpt.txt contains only a short-window summary of allowlisted tags, recording at least the tag, level, elapsed realtime, and redacted message.

Record the actual clock domain, units, and an identifier tying events to the same boot. Android elapsedRealtimeNanos uses CLOCK_BOOTTIME, including suspend; JankStats frameStartNanos uses the uptime/System.nanoTime domain and cannot simply be renamed elapsed_realtime. Conversion requires contemporaneous clock pairs or ClockSnapshots; a stale fixed offset fails across suspend. Perfetto does not automatically normalize external JSON, and trace start/end timestamps alone do not establish the mapping. Let the SDK timestamp ordinary Track Events; supply explicit timestamps only with a verified conversion.

Do not truncate trace.perfetto-trace directly on the app side. A Perfetto trace is a protobuf stream; crude binary trimming can damage packets, clock synchronization, and incremental state. Control the window at capture time instead: ring buffers bound the pre-trigger window, stop_delay_ms controls the post-trigger window, and trigger rate limits control repeated captures.

max_file_size_bytes is a hard file-size cap that stops tracing when reached. A long-running flight recorder with too small a cap can stop before the trigger. Use it with write_into_file / file_write_period_ms to control disk-write risks; it is not a window-trimming tool.

As the solution becomes a platform, encode quotas, privacy, and retry behavior in product upload rules:

  • Device-side quotas: rate-limit by user, device, version, trigger, and day.
  • Network conditions: upload by default only on Wi-Fi or user-approved networks; defer on low battery, abnormal temperature, or background restrictions.
  • Content allowlists: fix metadata fields, allow only listed log tags, and prohibit embedding user input in app trace sections.
  • Trace contents: minimize data sources, restrict processes/producers, and avoid logcat by default. ftrace, sched, Binder, thread names, and process names may themselves contain sensitive information.
  • Transport and retention: encrypt uploads, audit server access, give traces, log excerpts, and aggregated results separate TTLs, and delete them automatically on expiry.

The app supplies application phases, anomaly triggers, and metadata; the platform supplies controlled presets, triggers, and file management; the server supplies trust checks, summaries, and aggregation. After reproducing an incident, first check that these records cover the same operation window before aggregating results across devices.

Summary

The Perfetto SDK adds application semantics: the system knows that a thread is running; the SDK tells you which frame it is decoding, how deep the queue is, and where a request flows next.

A practical implementation order is to start with android.os.Trace in Java/Kotlin, use Track Event for long-term native instrumentation, and consider custom DataSources for complex structures. Stable event names, controllable categories, counters with units, and flows with stable IDs let SQL and reports remain reusable over time. For field tracing, the app must also design triggers, metadata, evidence packages, and upload policy together.

References

  1. Tracing SDK
  2. Track events
  3. Recording In-App Traces with Perfetto
  4. androidx.tracing.perfetto
  5. Trace configuration - Triggers
  6. android.os.Trace
  7. JankStats Library

This review checked the v58.3 SDK, category precedence, Track/CounterTrack, trigger API, and TraceConfig. AndroidX references include the Tracing 1.0.0 release record, FrameData, and SystemClock, and NDK ATrace APIs. The original September 19, 2026 source links remain below for comparison.

About Me and the Blog

Follow Android Performance.

CATALOG
  1. 1. Perfetto Series Catalog
  2. 2. When Application Semantics Are Needed
  3. 3. SDK Integration Boundaries
  4. 4. Define the Application-Phase Dictionary Before Writing Code
  5. 5. Two Backends Solve Different Problems
  6. 6. Minimal Integration Skeleton
  7. 7. Categories Are Production Controls
  8. 8. Capture Alongside System Traces
  9. 9. Use Flows or Independent Tracks for Cross-Thread Tasks
  10. 10. Counters Represent State, Not Logs
  11. 11. Event Names Are a SQL Interface
  12. 12. Control Write Volume
  13. 13. The App-Side Field Tracing Protocol
  14. 14. Trace Packages Need Context
  15. 15. Summary
  16. 16. References
  17. 17. About Me and the Blog