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

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 | // tracing_categories.h |
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 | // tracing_categories.cc |
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 | // tracing_init.cc |
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 | void DecodeFrame(int 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 thedebugtag when defining the category.pipeline.slow: events with higher overhead; add theslowtag 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 | buffers { |
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 | buffers { size_kb: 16384 fill_policy: RING_BUFFER } # 0: sched / ftrace |
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 | adb shell perfetto --query |
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 | INCLUDE PERFETTO MODULE slices.with_context; |
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 | INCLUDE PERFETTO MODULE slices.with_context; |
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 | trace_name,scenario,frame_or_request_id,phase,start_ms,dur_ms,thread_name,frame_jank_type,evidence_grade,missing_marker |
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 |
|
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 | void StartRequest(uint64_t 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 | void ReportDecodeQueueDepth(int 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 | // Avoid: each frame gets a different event name, making stable SQL aggregation difficult. |
A better naming pattern is “phase + action”:
DecodeFrame: decode an application frame, withframe_id,stream_id, andcodecarguments.UploadTexture: upload a texture or buffer, withframe_idandtexture_bytesarguments.RenderScene: submit a scene-rendering pass, withscene_idorsurface_idarguments.SubmitRequest: submit a cross-thread request, withrequest_idandqueue_deptharguments.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 | INCLUDE PERFETTO MODULE slices.with_context; |
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_kbandBufferExhaustedPolicy::kStallonly 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_*andtrack_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 | SELECT name, idx, severity, source, value |
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 | buffers { size_kb: 16384 fill_policy: RING_BUFFER } # 0: sched / ftrace |
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 | { |
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 | trace-package/ |
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 | { |
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 | { |
upload_policy.json specifies whether the field package can be uploaded and how long it is retained:
1 | { |
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
- Tracing SDK
- Track events
- Recording In-App Traces with Perfetto
- androidx.tracing.perfetto
- Trace configuration - Triggers
- android.os.Trace
- 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.