CN

Android Perfetto Series 16: GPU, Power Counters, and Hardware Bottleneck Anal...

Word count: 7.1kReading time: 44 min
2026/05/04
loading

When FrameTimeline marks a late frame but the main thread and RenderThread show no obvious long tasks, GPU work and fences are useful next checks. GPU frequency, battery current, and power rails describe hardware state in that window; they do not establish on their own why an app frame presented late.

Parts 07 and 08 covered the rendering path from the App to SurfaceFlinger; Parts 06 and 09 covered refresh rates and CPU scheduling/frequency. This article adds an often-skipped step without repeating those topics: when a problem appears to have reached the hardware resource layer, how should GPU, devfreq, battery counters, and power rails enter the analysis of the same trace interval?

These counters are difficult because not every device provides them, and different GPU vendors use different counter names. A conclusion cannot simply say “a high GPU value means a GPU bottleneck.” FrameTimeline, RenderThread, SurfaceFlinger, the GPU timeline, frequency, and power data must be examined over the same interval.

Version note (reviewed October 2026): Configurations, descriptors, import behavior, and SQL are checked against Perfetto v58.3. Android version alone guarantees neither GPU producers/counters nor power rails. Record device, driver, capture-side, and parser versions separately. Example counter IDs and windows must be bound to actual evidence.

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

This article addresses practical questions: which hardware data is worth capturing, how to narrow conclusions when it is unavailable, and how to avoid misinterpretation when it is available. You do not need to become a GPU driver engineer. You do need to define the limits of an investigation: when the UI is late, threads are not busy, and composition is not blocked, which hardware signal should you examine next?

Hardware signals aligned to the same window

Diagram: frames, GPU stages, frequency, and average power aligned to one window. The bottom line represents power derived from energy differences, not decreasing cumulative energy or measured instantaneous power. Overlap alone does not establish a GPU root cause.

When Hardware Counters Are Needed

FrameTimeline can identify a janky frame. CPU scheduling can show whether a thread obtained CPU time. RenderThread/HWUI can help determine whether the App completed drawing and produced or submitted a buffer within budget. GPU execution, completion, and hardware pressure require a combined reading of FrameTimeline, GPU render stages, fences, GPU counters, or AGI/vendor tools.

Hardware counters address the later part of that investigation:

Symptom What counters can add
RenderThread submits on time, but the frame is still late GPU render stages, GPU completion, GPU frequency
GPU frequency rises, but frames remain slow GPU counters, GPU memory, devfreq / interconnect / memory-related device frequency tracks
Frame rate drops after running for a while Power rails, battery current, thermal logs, changes to frequency caps
Abnormal power consumption without obvious UI jank Power rails, battery counters, screen state, continuous animation/wakeups
Heavy load in a game or player AGI GPU counters, Perfetto system context, application Track Events

Hardware counters rarely identify a root cause on their own. They are more useful for ruling out unpromising directions: is the problem on the CPU, GPU, composition, memory-bandwidth, or power/thermal-policy side?

First, distinguish these terms:

  • frequency is a frequency track. It shows the operating level selected by a frequency-scaling policy, not utilization.
  • A counter is a sampled value exposed by hardware or a driver. It may represent a percentage, bytes, an event count, or a vendor-defined unit.
  • A render stage is an interval of GPU work. It helps determine whether GPU work exceeds a frame budget, but depends on producer and driver support.
  • battery current is measured at the battery and covers the whole device. It is not an individual App’s power consumption.
  • A power rail measures subsystem energy. It can show changes in rails such as GPU, Display, Memory, and WLAN, but still does not provide per-process measurements.

Read hardware counters together with frames, threads, SurfaceFlinger, and application actions from the same interval. No single track, however clear, can establish attribution by itself.

When I see a high GPU frequency track, I treat it as a reason to inspect the frame, not as a verdict that the GPU held it up. A frequency policy can react to load without telling us which stage missed the deadline. Align render stages and fences with App and SurfaceFlinger frames before assigning the delay.

Keep the Data Types Separate

Data TraceConfig collection entry point PerfettoSQL module/table Questions it can answer Limitations
GPU render stages gpu.renderstages or a producer with a vendor suffix GPU tracks such as gpu_slice Whether GPU work extends beyond the target window Depends on GPU producer and driver support
GPU frequency linux.ftrace + power/gpu_frequency, or linux.sys_stats polling stdlib android.gpu.frequency / table android_gpu_frequency GPU frequency scaling, caps, and frequency state under thermal control Frequency is not utilization; label units according to the device and track
GPU memory linux.ftrace + gpu_mem/gpu_mem_total stdlib android.gpu.memory / table android_gpu_memory_per_process Per-process GPU memory trends and possible texture/buffer/Surface scale issues Memory usage is neither bandwidth nor per-frame GPU activity
GPU counters gpu.counters, gpu.counters.adreno, etc. counter, gpu_counter_track Hardware events involving fragments, vertices, textures, memory, etc. Counter IDs/names depend on the device
GPU work period linux.ftrace + power/gpu_work_period Module android.gpu.work_period; table android_gpu_work_period_track UID/GPU work periods on some devices Not equivalent to per-frame completion; attribution to a frame/layer still needs tokens, render stages, or fences
Battery counters android.power counter Changes in battery current, voltage, and charge Whole-device measurements affected by USB/charging state, not App power consumption
Power rails android.power + collect_power_rails stdlib android.power_rails Energy changes in rails such as GPU, Display, and Memory Depends on hardware and the IPowerStats HAL; not per-process

android_gpu_work_period_track is a track-dimension table providing attribution fields such as id/uid/ugpu/gpu_id; it does not contain work intervals. Join slice using slice.track_id = android_gpu_work_period_track.id and inspect ts/dur for those intervals. Do not assume there is an android_gpu_work_period table, or treat an interval directly as the GPU completion time of an individual frame.

The official Perfetto GPU documentation also mentions data sources such as vulkan.memory_tracker and gpu.log. These serve specialized graphics investigations and should not be included in a default long trace. Locate the problematic interval with minimal system context first, then decide whether to add specialized data sources.

Device Support Comes First

GPU counters are not a universal Android capability. Keep these constraints from the official documentation in mind:

  • A GPU producer may register a data source name with a hardware suffix, such as gpu.counters.adreno or gpu.renderstages.mali. TraceConfig must use the exact name.
  • GPU counters are selected using device-specific counter IDs or counter names. Available IDs/names come from the descriptor exposed by the GPU producer.
  • Choose either counter_ids or counter_names; do not mix them. Only some producers support counter_names and glob selection. Check supports_counter_names / supports_counter_name_globs in the descriptor.
  • Android GPU frequency can be captured through the ftrace event power/gpu_frequency.
  • Per-process GPU memory can be captured through the ftrace event gpu_mem/gpu_mem_total.

Power counters have similar limitations. Battery counters depend on device power-management hardware and are more common on Pixel devices. Power rails depend on specialized hardware such as ODPM (on-device power rail monitor) and the IPowerStats HAL. Availability still needs to be checked per device on Android 12–17; many production devices lack it.

Android Studio Power Profiler documents ODPM support on Pixel 6 and later Pixels, with rail sets varying by device. This is not a claim about every Android 12+ device. Perfetto access also depends on hardware, HAL, permissions, and capture configuration; empty metadata alone does not prove unsupported hardware.

Confirm Device Capabilities First

Do not write a GPU/Power configuration and hope the device happens to support it. First identify registered producers and data-source names. In v58.3, --query --long is a human-readable service listing; --long mainly expands category text and does not print complete GPU counter descriptors. For counter IDs, units, and name-selection support, use available UI capability information or decode --query-raw with the matching proto:

1
2
3
4
adb shell perfetto --query --long
adb exec-out perfetto --query-raw > service-state.pb || exit 1
protoc --proto_path=. --decode=perfetto.protos.TracingServiceState \
protos/perfetto/common/tracing_service_state.proto < service-state.pb || exit 1

The first command is for manual name discovery. The next two require host protoc and a Perfetto source checkout as the working directory. Inspect decoded ds_descriptor.gpu_counter_descriptor for specifications and supports_counter_names/supports_counter_name_globs. A producer not yet running/registered can be absent. Confirm rail metadata after a short capture; registration of android.power does not establish rail availability.

GpuCounterSpec expresses units through numerator_units/denominator_units. Also read value_direction: in v58.3 the unspecified default looks backward, as commonly used for GPU measurements ending at the timestamp; explicit FORWARDS_LOOKING applies afterward. Do not copy the forward-held frequency-state model directly to interval measurements such as GPU Busy.

Keep a capability inventory in the report so that “unsupported device,” “configuration name mismatch,” “buffer data loss,” and “signal did not rise” remain separate:

This is an illustrative capture observation, not a universal device list. Support true with descriptors/observed data. Record empty rail metadata as null with a reason until unsupported capability is established.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"has_gpu_renderstages": true,
"gpu_renderstages_source_name": "gpu.renderstages.mali",
"has_gpu_counters": true,
"gpu_counter_source_name": "gpu.counters.mali",
"gpu_counter_selection_mode": "counter_ids",
"selected_counter_ids_or_names": [1, 3, 106],
"has_gpu_freq": true,
"has_gpu_mem": true,
"has_power_rails": null,
"has_battery_current": true,
"has_thermal": true,
"missing_reason": "no_usable_power_rail_metadata_in_this_capture"
}

Use separate tiers of field presets as well:

Preset Data sources Scenario Upload and fallback rules
baseline_jank FrameTimeline, App markers, sched, CPU/GPU freq, idle First pass in production or an ordinary field investigation May be included in a field package; without GPU stages, explicitly record missing evidence and limit findings to hardware trends
gpu_suspect_short Baseline + render stages, GPU memory, devfreq, short-interval power/battery polling A 10–30-second reproduction window Short windows only; downgrade conclusions if stats report data loss
gpu_counter_lab GPU counters, AGI/vendor counters, necessary system context Specialized laboratory investigation No default upload; record counter ID/name/unit and the device descriptor

A Minimal Signal Set for Short Traces

This example uses an authorized shell/system consumer. App-side Track Event or ATrace instrumentation does not itself grant ftrace, android.power, or GPU capture access. Constrained APIs such as ProfilingManager have separate version and policy rules; they are not authorization for arbitrary TraceConfigs.

Build the first configuration on the UI jank baseline from Part 15: FrameTimeline, ATrace / Track Event for the target package, and sched/freq/idle form the primary attribution path; GPU/Power adds supporting signals. This short-trace configuration is for reproduction windows only. For long-running field tracing, reuse the file writing, file limits, and trigger approach from Part 15.

Tailor this template to verified capabilities. Replace the renderstages placeholder, package, and actual producer filters; remove an unavailable source block and record that limitation. An unreplaced but syntactically valid source name can simply match nothing, so command success does not establish captured stages.

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
duration_ms: 10000

buffers {
size_kb: 65536
fill_policy: RING_BUFFER
}

data_sources {
config {
name: "linux.ftrace"
ftrace_config {
compact_sched {
enabled: true
}
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
ftrace_events: "power/cpu_frequency"
ftrace_events: "power/cpu_idle"
ftrace_events: "power/gpu_frequency"
ftrace_events: "power/gpu_work_period"
ftrace_events: "gpu_mem/gpu_mem_total"
ftrace_events: "devfreq/devfreq_frequency"

atrace_categories: "gfx"
atrace_categories: "view"
atrace_categories: "wm"
atrace_categories: "hal"
atrace_categories: "binder_driver"
atrace_apps: "com.example.app"
}
}
}

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

data_sources {
config {
# Example placeholder: replace with the exact gpu.renderstages* name exposed by the descriptor.
name: "<gpu.renderstages descriptor name>"
}
}

data_sources {
config {
name: "track_event"
track_event_config {
disabled_categories: "*"
enabled_categories: "ui"
enabled_categories: "app"
}
}
producer_name_filter: "com.example.app"
producer_name_regex_filter: "com\\.example\\.app(:.*)?"
}

data_sources {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
}
}
}

data_sources {
config {
name: "linux.sys_stats"
sys_stats_config {
stat_period_ms: 1000
stat_counters: STAT_CPU_TIMES
cpufreq_period_ms: 500
cpuidle_period_ms: 500
gpufreq_period_ms: 500
devfreq_period_ms: 500
thermal_period_ms: 1000
}
}
}

data_sources {
config {
name: "android.power"
android_power_config {
battery_poll_ms: 250
battery_counters: BATTERY_COUNTER_CAPACITY_PERCENT
battery_counters: BATTERY_COUNTER_CHARGE
battery_counters: BATTERY_COUNTER_CURRENT
battery_counters: BATTERY_COUNTER_VOLTAGE
collect_power_rails: true
}
}
}

The aim is to collect a complete set of evidence for the same window first. FrameTimeline requires Android 12 or later. gpu.renderstages depends on GPU producer and driver support and is unavailable on many devices. If render stages cannot be captured, report “GPU stage evidence is missing.” Do not promote frequency or power rail measurements directly into a conclusion about GPU execution.

Be explicit when GPU stages are missing: gpu_stage_status=unavailable; conclusion_scope=hardware_trend_only; cannot_claim=gpu_execution_delay.

Remember to pair stat_period_ms with explicit stat_counters. Leaving stat_counters empty may collect all /proc/stat counters, including per-CPU times and per-IRQ counts. Specifying STAT_CPU_TIMES makes the intended data clear and avoids pulling hundreds of IRQ counters into the trace.

power/gpu_frequency and devfreq/devfreq_frequency are event paths; gpufreq_period_ms and devfreq_period_ms are sysfs polling paths. Events provide precise change points, but you may need to look before the window for the preceding state. Polling captures sustained trends but is constrained by its sampling period and may miss brief spikes. power/cpu_idle records transitions; cpuidle_period_ms polls idle-state durations. USB connections, wakelocks, and whether a scenario is in the foreground or background all affect idle measurements.

Thermal-zone names, units, and availability vary by device. If thermal_period_ms was not collected or there are no thermal tracks, report only “this trace contains no thermal evidence,” not “thermal control had no effect.”

These polling periods suit only short diagnostic windows. Long traces require longer sampling periods, separate buffers, and measurements of write rate and CPU overhead. These power counters are not a Power HAL trace. Unless you also capture the Power HAL’s own atrace, logs, or vendor events, report only “frequency, rail, and thermal changes occurred in the same window,” not “the Power HAL caused it.”

Android 12+ generally permits configs in /data/misc/perfetto-configs, subject to device permissions. Resolve placeholders first and stop on capture failure to avoid pulling an old file at the same path. Use a separate output name per run. Text versus binary proto encoding changes neither authorization nor source availability:

1
2
3
adb push gpu_power.pbtxt /data/misc/perfetto-configs/gpu_power.pbtxt || exit 1
adb shell perfetto --txt -c /data/misc/perfetto-configs/gpu_power.pbtxt -o /data/misc/perfetto-traces/gpu_power.perfetto-trace || exit 1
adb pull /data/misc/perfetto-traces/gpu_power.perfetto-trace || exit 1

For short traces, these signals can share one buffer. For long traces, separate them and examine their data volumes. Combining 1 ms GPU counter sampling, logcat, Track Event, and ftrace sched can quickly shrink the ring-buffer window. Reuse the constraints on data loss and buffer windows from Part 12 and on long-running file output from Part 15.

GPU Counter Selection Examples

Select GPU counters for the particular device. Selecting counter IDs from its descriptor is the more robust approach; use counter_names and globs only when the producer descriptor explicitly supports them.

The following illustrates the configuration shape only. If the descriptor exposes a suffixed name such as gpu.counters.adreno or gpu.counters.mali, use that exact name. name, counter_ids, and counter_names must all match what the target device supports.

1
2
3
4
5
6
7
8
9
10
11
data_sources {
config {
name: "gpu.counters"
gpu_counter_config {
counter_period_ns: 1000000
counter_ids: 1
counter_ids: 3
counter_ids: 106
}
}
}

If the producer supports name-based selection, the configuration can look like this:

1
2
3
4
5
6
7
8
9
10
11
data_sources {
config {
name: "gpu.counters.adreno"
gpu_counter_config {
counter_period_ns: 1000000
counter_names: "GPU % Busy"
counter_names: "*Fragment*"
counter_names: "*Texture*"
}
}
}

counter_period_ns is a requested period, not a guarantee of independent valid samples every millisecond. Hardware counters may have mutually exclusive groups, multiplexing, or minimum-period limits. Check descriptors/vendor contracts and actual timestamps. Merge these data_sources fragments into a complete buffer/duration config rather than starting them as indefinite captures.

Dividing the Work Between AGI and Perfetto

AGI Frame Profiler captures Vulkan directly; its OpenGL ES path translates through ANGLE, so replay/translated results are not automatically equivalent to the original driver path. System and frame profiling also have different device/API coverage. Validate the device and select supported capabilities. Official Frame Profiler overview

AGI is better suited to questions inside the graphics pipeline:

  • Which render passes, draw calls, shaders, and pipeline states make up a frame?
  • How should specialized GPU counters for fragments, vertices, textures, and bandwidth be interpreted?
  • How can rendering be optimized at the Vulkan/OpenGL level?

Perfetto is better suited to system timing relationships:

  • Is there unusual behavior in the main thread, RenderThread, SurfaceFlinger, or Binder around the problematic frame?
  • Do GPU render stages run beyond the App/SF target window?
  • Do GPU/CPU frequency, power rails, and thermal logs change at the same time?
  • Does the issue occur only with the screen on, during refresh-rate changes, as temperature rises, or under background contention?

Games, maps, video, and demanding Flutter/WebView scenarios therefore often need both sets of evidence: Perfetto supplies system context, and AGI supplies GPU internals.

Check Trace Health First

After obtaining a trace, do not immediately interpret missing tracks as “device unsupported.” Part 12 covers the general data-loss check, including stats for buffer packet loss and ftrace enablement failures and how to handle them. Run that query first. GPU and power data also have their own parsing stats: GPU counter specification errors, render-stage parsing errors, and power-rail parsing errors can all make signals silently disappear. Use this additional query for domain-specific entries:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT name, idx, severity, source, value
FROM stats
WHERE value != 0
AND (
severity IN ('error', 'data_loss')
OR name IN (
'gpu_counters_invalid_spec',
'gpu_counters_missing_spec',
'gpu_render_stage_parser_errors',
'power_rail_empty_packet',
'power_rail_unknown_index'
)
)
ORDER BY name, idx;

The meaning of idx depends on the statistic: traced_buf_* usually uses buffer indices, ftrace CPU entries use CPU numbers, and non-indexed entries may be null. Do not map every idx to a buffer. Distinguish three cases in the report: the device did not expose a signal, the configuration did not match a producer, or buffer policy/data loss discarded the signal. Only after checking configuration and trace health can you narrow the conclusion to “this device or this trace did not provide a usable signal.”

Identify the Counters Present in the Trace

First, list all counter tracks:

1
2
3
4
5
6
7
8
9
10
SELECT
t.id AS track_id, t.machine_id, t.type, t.unit,
t.name,
COUNT(c.id) AS samples,
MIN(c.ts) / 1e9 AS first_s,
MAX(c.ts) / 1e9 AS last_s
FROM counter_track t
LEFT JOIN counter c ON c.track_id = t.id
GROUP BY t.id, t.machine_id, t.type, t.unit, t.name
ORDER BY samples DESC;

Query gpu_counter_track for GPU counters. Its unit column is often empty in v58.3: retain unknown and verify descriptor numerator/denominator units and the import path instead of guessing from names. Keep descriptions and GPU identity too:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT
g.id AS track_id,
g.ugpu,
g.gpu_id,
g.name,
g.unit,
g.description,
COUNT(c.id) AS samples,
MIN(c.ts) / 1e9 AS first_s,
MAX(c.ts) / 1e9 AS last_s
FROM gpu_counter_track g
LEFT JOIN counter c ON c.track_id = g.id
GROUP BY g.id, g.ugpu, g.gpu_id, g.name, g.unit, g.description
ORDER BY samples DESC;

For reports spanning multiple GPUs or devices, include GPU metadata:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT
gct.id AS track_id,
gct.name AS counter_name,
gct.unit,
gct.description,
gct.gpu_id AS raw_gpu_id,
gpu.name AS gpu_name,
gpu.vendor,
gpu.model,
gpu.architecture,
gpu.machine_id
FROM gpu_counter_track gct
LEFT JOIN gpu ON gct.ugpu = gpu.id
ORDER BY gpu.machine_id, gct.gpu_id, gct.name;

If the target GPU / power counter is absent from the results, skip the specialized SQL that follows.

You can also list power-rail metadata first, so the report does not mistake a raw rail name for an application-level meaning:

1
2
3
4
5
6
7
8
9
INCLUDE PERFETTO MODULE android.power_rails;

SELECT
track_id, machine_id, power_rail_name,
raw_power_rail_name,
friendly_name,
subsystem_name
FROM android_power_rails_metadata
ORDER BY subsystem_name, power_rail_name;

If this table is empty, do not write “the GPU rail did not rise.” Write instead: this device or this trace did not provide usable power-rail metadata.

Where the Problem Window Comes From

GPU/Power queries must use the same problem window. Record its source in the report: frametimeline, track_event, trigger, or manual annotation.

Identify the specific FrameTimeline slice ID for the target app/layer and replace NULL below with it. Do not automatically pick the first jank from a same-named process. An unclosed frame cannot define a complete frame window. Five seconds before and one second after are investigation choices; retain requested bounds alongside the bounds available in the trace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WITH selected_frame(id) AS (VALUES (NULL)),
jank_frame AS (
SELECT a.id, a.upid, a.ts, a.dur, a.layer_name, p.name AS process_name,
a.jank_type, a.display_frame_token, a.surface_frame_token
FROM actual_frame_timeline_slice a
JOIN process p USING (upid)
WHERE a.id = (SELECT id FROM selected_frame)
AND a.dur >= 0
AND COALESCE(a.jank_type, '') NOT IN ('', 'None')
)
SELECT 'frametimeline' AS window_source, id AS frame_slice_id, upid,
ts - 5000000000 AS requested_start_ts,
ts + dur + 1000000000 AS requested_end_ts,
MAX(trace_start(), ts - 5000000000) AS start_ts,
MIN(trace_end(), ts + dur + 1000000000) AS end_ts,
display_frame_token, surface_frame_token, layer_name,
process_name, jank_type
FROM jank_frame;

Use a stable FieldTrigger/app_ui_jank name and place event_id in a debug annotation. This example targets default thread tracks: fill the target upid and event_id, then investigate duplicates rather than picking LIMIT 1. Restore process context separately for custom tracks. Do not hardcode a zero matching delta before validating external JSON clock/event alignment:

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

WITH target(upid, event_id) AS (VALUES (NULL, NULL))
SELECT 'track_event' AS window_source,
s.id AS marker_slice_id, s.upid, s.utid,
EXTRACT_ARG(s.arg_set_id, 'debug.event_id') AS event_id,
COUNT(*) OVER () AS matching_marker_rows,
s.ts - 5000000000 AS requested_start_ts,
s.ts + MAX(s.dur, 1000000000) AS requested_end_ts,
MAX(trace_start(), s.ts - 5000000000) AS start_ts,
MIN(trace_end(), s.ts + MAX(s.dur, 1000000000)) AS end_ts,
s.name AS marker_name
FROM thread_slice s
JOIN target t ON s.upid = t.upid
WHERE s.name = 'FieldTrigger/app_ui_jank'
AND EXTRACT_ARG(s.arg_set_id, 'debug.event_id') = t.event_id
AND s.dur >= 0;

Without a window source, you can scan only for global trends. Do not attribute a GPU/Power fluctuation to a particular scroll or frame.

Querying GPU Render Stages

The analysis step “check whether GPU work extends beyond the target window” uses the gpu.renderstages data source. Support is much narrower than for frequency, and many devices have no corresponding producer. Always start by confirming that this trace actually contains render-stage tracks:

1
2
3
SELECT id, name, machine_id, parent_id, dimension_arg_set_id
FROM track
WHERE type = 'gpu_render_stage';

Older articles and scripts often use SELECT id, name FROM gpu_track WHERE scope = 'gpu_render_stage'. This still works today, but gpu_track is marked deprecated in Perfetto source: it mixes unrelated tracks such as drm, mali, render stage, vulkan, and gpu_log in one table. Its scope column is also just an alias for type. New scripts should query track.type directly.

If the query returns zero rows, stop here and return to “Device Support Comes First.” Report gpu_stage_status=unavailable; do not substitute frequency or rail data.

Once tracks exist, list stage candidates in the window, retaining slice_id, track_id, machine_id, and raw nanoseconds. This scans captured render-stage tracks; filter actual GPU/queue ownership when several are present. Summing overlapping durations from different queues is not GPU utilization:

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
WITH target_window(start_ts, end_ts) AS (
VALUES (NULL, NULL)
),
stage_tracks AS (
SELECT id, name, machine_id
FROM track
WHERE type = 'gpu_render_stage'
)
SELECT
s.id AS slice_id, s.track_id, st.machine_id,
s.ts AS start_ns, s.dur AS stage_dur_ns,
st.name AS queue_name,
s.name AS stage_name,
ROUND(s.ts / 1e6, 3) AS start_ms,
MIN(s.ts + s.dur, w.end_ts) - MAX(s.ts, w.start_ts) AS overlap_ns,
ROUND(s.dur / 1e6, 3) AS stage_dur_ms,
s.arg_set_id
FROM slice s
JOIN stage_tracks st ON s.track_id = st.id
JOIN target_window w
WHERE typeof(w.start_ts) = 'integer' AND typeof(w.end_ts) = 'integer'
AND w.end_ts > w.start_ts AND s.dur > 0
AND s.ts < w.end_ts
AND s.ts + s.dur > w.start_ts
ORDER BY overlap_ns DESC
LIMIT 30;

This query shows producer-reported stage intervals and their overlap with the window, not an app-specific GPU critical path by itself. Correlate CPU submission, GPU execution/waits, composition, and presentation using submission/context, frame/layer, or fence evidence. GPU pressure can instead be concurrent competition. Inspect actual slice-argument keys before defining joins:

1
2
3
4
5
6
SELECT DISTINCT a.flat_key
FROM slice s
JOIN track t ON s.track_id = t.id
JOIN args a ON a.arg_set_id = s.arg_set_id
WHERE t.type = 'gpu_render_stage'
ORDER BY a.flat_key;

Record two limitations in script comments. First, the GPU producer determines track names, stage names, and args keys. Adreno and Mali output differs, so scripts intended for multiple devices must not hardcode those names. Second, newer trace processor versions organize render-stage tracks by internal track type; gpu_track is only a compatibility view for older scripts. If a query reports no such table/column, use the three-way diagnosis from Part 11 and start with PRAGMA table_info, rather than changing application logic first.

GPU Frequency and GPU Memory

Fill every subsequent NULL window with the same investigation’s integer-nanosecond bounds; unset inputs return no analysis, rather than substituting arbitrary 30–45-second data. Frequency/memory intervals generally assume a value holds until the next sample, with the final value possibly extended to trace_end. This is a state model, not proof of complete observation.

android.gpu.frequency selects GPU counters named gpufreq and creates forward intervals, but its output drops track/ugpu/machine identity. The query below retains those fields while using the same track-selection/interval rules and clipping to the target window. Filter the verified GPU when interpreting results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
INCLUDE PERFETTO MODULE counters.intervals;

WITH target_window(start_ts, end_ts) AS (VALUES (NULL, NULL)),
freq_intervals AS (
SELECT * FROM counter_leading_intervals!((
SELECT c.* FROM counter c
JOIN gpu_counter_track g ON g.id = c.track_id
WHERE g.name = 'gpufreq'
))
)
SELECT c.track_id, g.ugpu, g.gpu_id, gpu.machine_id,
g.unit, c.value AS gpu_freq_raw,
c.ts AS source_ts_ns, c.dur AS source_dur_ns,
MAX(c.ts, w.start_ts) AS clipped_start_ns,
MIN(c.ts + c.dur, w.end_ts) - MAX(c.ts, w.start_ts) AS overlap_ns
FROM freq_intervals c
JOIN gpu_counter_track g ON c.track_id = g.id
LEFT JOIN gpu ON g.ugpu = gpu.id
CROSS JOIN target_window w
WHERE typeof(w.start_ts) = 'integer' AND typeof(w.end_ts) = 'integer'
AND w.end_ts > w.start_ts AND c.dur > 0
AND c.ts < w.end_ts AND c.ts + c.dur > w.start_ts
ORDER BY c.track_id, c.ts;

The unit of gpu_freq_raw must follow the current device and track output, and the report must state it. Do not compare raw numbers directly across devices.

The GPU-memory library represents observed per-process state. This query finds the maximum observed value within the window in MiB, not a guaranteed between-sample peak, exclusive physical VRAM, or complete graphics-memory accounting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
INCLUDE PERFETTO MODULE android.gpu.memory;

WITH target_window(start_ts, end_ts) AS (VALUES (NULL, NULL))
SELECT
p.upid,
p.name AS process_name,
MAX(g.gpu_memory) AS max_observed_gpu_mem_bytes,
MAX(g.gpu_memory) / 1048576.0 AS max_observed_gpu_mem_mib
FROM android_gpu_memory_per_process g
JOIN process p USING (upid)
JOIN target_window
WHERE typeof(start_ts) = 'integer' AND typeof(end_ts) = 'integer'
AND end_ts > start_ts
AND g.dur > 0
AND g.ts < target_window.end_ts
AND g.ts + g.dur > target_window.start_ts
GROUP BY p.upid, p.name
ORDER BY max_observed_gpu_mem_mib DESC
LIMIT 20;

High GPU frequency means only that policy raised the frequency, not that the GPU is fully utilized. High GPU memory does not establish a memory-bandwidth bottleneck either; it suggests directions such as textures, buffers, or Surface counts. devfreq represents frequency nodes exposed by the device/kernel. List raw names first, then interpret them using the SoC, kernel nodes, and vendor documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
WITH target_window(start_ts, end_ts) AS (VALUES (NULL, NULL))
SELECT
t.id AS track_id, t.machine_id, t.unit,
t.name AS devfreq_track_name,
COUNT(*) AS samples,
MIN(c.ts) / 1e9 AS first_s,
MAX(c.ts) / 1e9 AS last_s,
MIN(c.value) AS min_value_raw,
MAX(c.value) AS max_value_raw
FROM counter c
JOIN counter_track t ON c.track_id = t.id
JOIN target_window
WHERE typeof(start_ts) = 'integer' AND typeof(end_ts) = 'integer'
AND end_ts > start_ts
AND LOWER(t.name) GLOB '*devfreq*'
AND c.ts >= start_ts
AND c.ts < end_ts
GROUP BY t.id, t.machine_id, t.unit, t.name
ORDER BY samples DESC;

Power Rails and Battery Counters

Power-rail energy is cumulative in uWs (microjoules). In v58.3, library average_power uses the forward next_value interval, while energy_delta is backward-looking; do not mix them. To expose sample gaps explicitly, this query pairs raw counter samples by track and estimates overlapping energy from their difference. Set max_gap_ns from the sampling cadence, actual timestamps, and acceptance policy, not a universal threshold:

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
INCLUDE PERFETTO MODULE android.power_rails;

WITH target_window(start_ts, end_ts, max_gap_ns) AS (
VALUES (NULL, NULL, NULL)
),
raw_samples AS (
SELECT c.id, c.track_id, c.ts, c.value,
COUNT(*) OVER (PARTITION BY c.track_id, c.ts) AS timestamp_rows
FROM counter c
JOIN android_power_rails_metadata m ON c.track_id = m.track_id
),
pairs AS (
SELECT *,
LEAD(ts) OVER (PARTITION BY track_id ORDER BY ts, id) AS next_ts,
LEAD(value) OVER (PARTITION BY track_id ORDER BY ts, id) AS next_value,
LEAD(timestamp_rows) OVER (PARTITION BY track_id ORDER BY ts, id) AS next_rows
FROM raw_samples
),
accepted AS (
SELECT p.track_id, p.next_ts - p.ts AS pair_gap_ns,
MIN(p.next_ts, w.end_ts) - MAX(p.ts, w.start_ts) AS overlap_ns,
(p.next_value - p.value) *
(1.0 * (MIN(p.next_ts, w.end_ts) - MAX(p.ts, w.start_ts)) /
(p.next_ts - p.ts)) AS overlap_energy_uWs
FROM pairs p CROSS JOIN target_window w
WHERE p.timestamp_rows = 1 AND p.next_rows = 1
AND p.next_ts > p.ts AND p.next_ts - p.ts <= w.max_gap_ns
AND p.next_value >= p.value
AND p.ts < w.end_ts AND p.next_ts > w.start_ts
)
SELECT m.track_id, m.machine_id, m.power_rail_name,
m.raw_power_rail_name, m.friendly_name, m.subsystem_name,
COUNT(a.track_id) AS valid_pairs,
w.end_ts - w.start_ts AS requested_dur_ns,
COALESCE(SUM(a.overlap_ns), 0) AS covered_dur_ns,
1.0 * COALESCE(SUM(a.overlap_ns), 0) /
(w.end_ts - w.start_ts) AS coverage_ratio,
MAX(a.pair_gap_ns) AS max_pair_gap_ns,
SUM(a.overlap_energy_uWs) AS energy_delta_uWs,
SUM(a.overlap_energy_uWs) * 1e6 / SUM(a.overlap_ns) AS avg_power_mw
FROM android_power_rails_metadata m CROSS JOIN target_window w
LEFT JOIN accepted a ON a.track_id = m.track_id
WHERE typeof(w.start_ts) = 'integer' AND typeof(w.end_ts) = 'integer'
AND typeof(w.max_gap_ns) = 'integer'
AND w.end_ts > w.start_ts AND w.max_gap_ns > 0
GROUP BY m.track_id, m.machine_id, m.power_rail_name,
m.raw_power_rail_name, m.friendly_name, m.subsystem_name,
w.start_ts, w.end_ts
ORDER BY avg_power_mw DESC;

energy_delta_uWs assumes constant power within each accepted sample pair. avg_power_mw averages only accepted overlapping durations; gaps are not zero-filled. valid_pairs counts pairs, not original samples. Report requested duration, coverage ratio, and maximum gap, while still checking stats, clocks, and device behavior. Do not extrapolate energy beyond the last sample or integrate across a counter reset. Rails can overlap in scope, so summing them does not necessarily give device power.

Battery counters can be queried directly from raw counters, but units must be labeled:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WITH target_window(start_ts, end_ts) AS (VALUES (NULL, NULL))
SELECT
c.track_id, t.machine_id, c.ts AS ts_ns,
c.ts / 1e9 AS ts_s,
t.name,
c.value,
CASE
WHEN t.name GLOB '*current_ua' THEN 'uA'
WHEN t.name GLOB '*current.avg_ua' THEN 'uA'
WHEN t.name GLOB '*charge_uah' THEN 'uAh'
WHEN t.name GLOB '*capacity_pct' THEN '%'
WHEN t.name GLOB '*voltage_uv' THEN 'uV'
ELSE 'unknown'
END AS unit
FROM counter c
JOIN counter_track t ON c.track_id = t.id
JOIN target_window
WHERE typeof(start_ts) = 'integer' AND typeof(end_ts) = 'integer'
AND end_ts > start_ts
AND t.name GLOB 'batt.*'
AND c.ts >= start_ts
AND c.ts < end_ts
ORDER BY c.track_id, c.ts;

There is another laboratory trap: USB connections affect battery current. The official documentation also warns that a connected USB cable may produce positive current and rising charge, indicating that the device is charging. For power comparisons, control USB power, separate the data and power connections, or use battery current only as a trend reference.

In v58.3, current.avg_ua is device-reported average current; its averaging window is not automatically this query’s window. current_ua is a different measurement. Counter track names take the form batt.<counter>. When the device reports a battery name, they become batt.<battery_name>.<counter>. Determine units with suffix GLOB patterns rather than hardcoding full names.

batt.current_ua measures current at the whole-device battery: negative values usually indicate battery discharge, while positive values indicate that external power is charging the battery. This sign describes battery current only; it does not represent the power consumption of a particular rail or App.

Prefer time-weighted statistics over stable scenarios. With uneven sampling, a sample AVG or median is not a time-weighted mean/median. Current is not power: account for voltage, supply path, and time. With USB power connected, battery current especially does not represent all device consumption.

Power analysis is closer to an A/B experiment than to locating individual janky frames. Control brightness, refresh rate, network, temperature, background tasks, USB power, and scenario duration, then compare two versions on the same device. Android Studio Power Profiler documentation likewise uses ODPM data to compare different implementations of the same feature, not to blame an App after a single sample.

Work Through the Same Window

For GPU/Power investigations, this sequence is more reliable:

  1. Use FrameTimeline to locate the problematic frame or window.
  2. Check whether the main thread, RenderThread, or SurfaceFlinger has already missed its timing target.
  3. Check whether GPU render stages / GPU completion extend beyond the target window.
  4. Check GPU frequency changes for boosts, reductions, or thermally constrained caps.
  5. Inspect GPU counters or AGI for directions such as fragment, texture, shader, or bandwidth pressure.
  6. Inspect power rails / battery current for power or temperature-rise constraints.
  7. Return to the system trace and check whether Binder, CPU contention, background animations, or refresh-rate changes explain the same interval.

Do not jump from a power-rail peak to “this App consumes excessive power.” Battery counters cover the whole device; power rails cover hardware subsystems. App attribution also requires foreground state, UID, threads, Surfaces, and application events.

Make Reports Machine-Readable

GPU/Power conclusions need a minimal schema so reviewers can see what evidence is missing and what the available evidence supports:

The JSON and A/B CSV are hypothetical and do not describe one shared device capture. Do not fill missing frame counts with refresh-rate × duration or infer a frequency cap from the observed maximum alone. Keep unverified states unknown.

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
{
"trace_name": "gpu_power_20260504_153012",
"device": "device_name",
"android_build": "fingerprint",
"gpu_vendor": "Qualcomm",
"gpu_model": "Adreno 740",
"scenario": "feed_scroll",
"window_source": "frametimeline",
"window_start_ms": 30000,
"window_end_ms": 45000,
"app_frame_count": 900,
"jank_count": 12,
"app_thread_status": "within_budget",
"rt_status": "within_budget",
"sf_status": "within_budget",
"gpu_stage_status": "available",
"gpu_freq_status": "high_relative_to_same_device_baseline",
"gpu_counter_status": "fragment_pressure",
"power_rail_status": "available",
"battery_status": "usb_power_connected",
"thermal_status": "nominal",
"stats_quality_level": "warning",
"fallback_used": "none",
"conclusion_scope": "gpu_direction",
"evidence_grade": "B",
"next_tool": "AGI"
}

For App teams, power investigations more often produce A/B reports than accusations based on a single trace:

1
2
3
run_id,group,device,brightness,refresh_rate,network,thermal_state,usb_power_state,scenario_duration_ms,avg_gpu_rail_mw,avg_display_rail_mw,avg_battery_current_ua,jank_rate,frame_count,confidence_notes
run-001,before,pixel,200nit,120hz,wifi,nominal,disconnected,300000,420,510,-820000,0.034,36000,stable temperature
run-002,after,pixel,200nit,120hz,wifi,nominal,disconnected,300000,360,508,-760000,0.031,36000,same preset

Report signals separately from actions available to the App:

Signal Possible App actions
Fragment/shader pressure Reduce overdraw, shader complexity, and the share of blur/shadows/effects
Texture/bandwidth pressure Reduce texture dimensions and format cost, upload frequency, and repeated sampling
High GPU memory Inspect Surface, texture, and buffer lifecycles and cache limits
SF jank / composition pressure Inspect layer counts, transparent layers, HWC/GPU composition, and BufferQueue
Power/thermal trend Reduce frame rate, animation duty cycle, and background rendering; adjust refresh-rate policy

Use consistent conclusion grades:

Grade Supported conclusion Claims to avoid
Strong conclusion Target frame/layer and GPU dependencies are linked, thread/SF and specialized evidence agree, and alternatives have been checked Attribution based only on frequency or rails
Correlated direction Frequency, devfreq, rails, and thermal signals change with the window, but GPU stages/counters are missing Presenting this as the root cause of an individual App frame
Missing evidence Descriptors, stats, metadata, or key tracks are missing Treating missing tracks as proof that no problem exists

Three Common Scenarios

GPU Pressure and Late Frames in the Same Window

A GPU-delay candidate requires the target frame/layer, app/RenderThread/SF dependencies, and related GPU stages/completion against that frame’s deadline. Rising frequency and counter pressure add context but are not prerequisites: the GPU may already be at high frequency or capped. Agreement within a window still does not exclude queueing, other processes, or upstream dependencies; validate through repeatable A/B tests or specialized tools when needed.

Also validate attribution using surface_frame_token / display_frame_token, layer_name, process, and the App/SF actual timelines. If the evidence cannot be associated with the current frame, layer, or process, report only “GPU-side pressure correlates with the problem window,” not “this App’s GPU work caused it.”

Render stages cannot independently replace fences. For display completion, also validate the present fence, FrameTimeline, and SurfaceFlinger timeline.

Still Slow After a Frequency Boost

Common directions include internal GPU bottlenecks, frequency pressure on memory/interconnect-related devices, a more complex SurfaceFlinger composition path, thermal throttling, and tight dependencies between stages in the App/SF/GPU pipeline. AGI or vendor GPU tools are especially useful here. Do not conclude from Perfetto frequency tracks alone.

Abnormal Power Consumption

Start with screen state, refresh rate, brightness, continuous animation, and background rendering, then inspect CPU/GPU/devfreq and power rails. Brief current peaks are not a sound basis for a conclusion. Combine this analysis with long traces or snapshots from Part 15 and check whether the same scenario reproduces consistently across runs.

Checks Before Writing the Report

Check How to report it
GPU counters, render stages, or power rails are missing Write “this trace has no corresponding evidence,” not “there is no GPU/power problem”
Counter names and semantics differ by GPU vendor Record device model, GPU, counter ID/name/unit, and descriptor source
Battery current covers the whole device Report whole-device trends; do not attribute them directly to an App
USB affects battery current Record USB/charging state and how power supply was controlled
GPU frequency is high or low Describe frequency state and cap changes; do not directly claim a GPU bottleneck or lack of response
Power rails measure subsystem energy Report the rail/subsystem, not an individual process’s consumption
GPU counters generate large data volumes Record sampling period, window length, and whether collection is restricted to the laboratory
The same counter is not comparable across GPU vendors State device and driver boundaries; do not compare raw values across vendors

Turn Hardware Signals into Reviewable Conclusions

GPU/Power counters supplement rendering analysis with hardware evidence. They cannot replace FrameTimeline, RenderThread, SurfaceFlinger, or CPU scheduling. They help determine whether the problem has reached the GPU, memory/interconnect, power, or thermal-control layer.

Follow three rules in reports: mark signals the device did not provide as missing rather than as counterevidence; identify a GPU/power direction only when multiple sources agree within the same window; and move to AGI or vendor GPU tools when the evidence points inside the GPU.

References

  1. GPU data sources
  2. Power data sources
  3. PerfettoSQL standard library
  4. AGI GPU performance counters
  5. Android Studio Power Profiler
  6. FrameTimeline
  7. CPU frequency and idle states
  8. Trace Processor Stats

The original b23c67be source links (September 19, 2026) remain below. This review also checks v58.3 service queries, GPU descriptors, GPU frequency, GPU memory, power rails, and counter intervals.

About Me and the Blog

Follow Android Performance.

CATALOG
  1. 1. Perfetto Series Catalog
  2. 2. When Hardware Counters Are Needed
  3. 3. Keep the Data Types Separate
  4. 4. Device Support Comes First
  5. 5. Confirm Device Capabilities First
  6. 6. A Minimal Signal Set for Short Traces
  7. 7. GPU Counter Selection Examples
  8. 8. Dividing the Work Between AGI and Perfetto
  9. 9. Check Trace Health First
  10. 10. Identify the Counters Present in the Trace
  11. 11. Where the Problem Window Comes From
  12. 12. Querying GPU Render Stages
  13. 13. GPU Frequency and GPU Memory
  14. 14. Power Rails and Battery Counters
  15. 15. Work Through the Same Window
  16. 16. Make Reports Machine-Readable
  17. 17. Three Common Scenarios
    1. 17.1. GPU Pressure and Late Frames in the Same Window
    2. 17.2. Still Slow After a Frequency Boost
    3. 17.3. Abnormal Power Consumption
  18. 18. Checks Before Writing the Report
  19. 19. Turn Hardware Signals into Reviewable Conclusions
  20. 20. References
  21. 21. About Me and the Blog