ai/skills/perfetto_workflow_android_heap_dump/SKILL.md
This skill is the recommended first pass when the user asks "what's wrong with this heap dump?", "why is this process using so much memory?", or "what's leaking?". It assumes the trace was recorded with the ART perfetto data source and contains at least one Java heap graph.
If the user has not yet loaded a trace into trace_processor, follow
the perfetto_infra_querying_traces skill first, then come back here.
Recording-side reference for heap dumps:
https://perfetto.dev/docs/data-sources/java-heap-profiler.
All queries below use
$upidand$tsas placeholders. Substitute the values you picked in step 1 —trace_processordoes not interpret$variables.
A heap dump is a snapshot of every Java object alive in a process at a moment in time, plus the references between them. To find a leak you generally want to answer two questions:
self_size. Retained size
is the memory that would be freed if the object went away.The Perfetto stdlib has prebuilt views for both. Reach for those before
joining heap_graph_object and heap_graph_reference by hand. Browse the
modules under android.memory.heap_graph.* in the stdlib reference:
https://perfetto.dev/docs/analysis/stdlib-docs.
INCLUDE PERFETTO MODULE android.memory.heap_graph.heap_graph_stats;
SELECT
upid,
graph_sample_ts,
total_heap_size,
reachable_heap_size,
total_obj_count,
reachable_obj_count,
anon_rss_and_swap_size,
oom_score_adj
FROM android_heap_graph_stats
ORDER BY graph_sample_ts;
Sanity checks at this point:
reachable_heap_size close to total_heap_size? A big gap means a
lot of memory is unreachable but not yet collected — interesting on its
own but not the typical "leak" pattern.total_heap_size compare to anon_rss_and_swap_size? If RSS
is much larger than the Java heap, the bloat is probably not Java —
consider native allocations instead.upid and graph_sample_ts you'll focus on for the rest of
the investigation. If there are multiple dumps, the earliest one
usually shows the steady state; later dumps show growth.Join with process to turn upid into a process name when reporting back
to the user — never expose raw upid values.
The android_heap_graph_class_summary_tree view aggregates the heap by
the shortest-path tree from GC roots, grouped by class name. It's the
fastest way to spot "this one class is holding most of the heap":
INCLUDE PERFETTO MODULE android.memory.heap_graph.class_summary_tree;
SELECT
name AS class_name,
root_type,
self_count,
self_size,
cumulative_count,
cumulative_size
FROM android_heap_graph_class_summary_tree
WHERE upid = $upid AND graph_sample_ts = $ts
ORDER BY cumulative_size DESC
LIMIT 30;
cumulative_size is the size of all objects of this class plus everything
they retain — this is what to sort by to find the dominant retainer.
self_size only counts the objects of the class itself.
Patterns that are usually worth flagging to the user:
cumulative_size that's a large fraction of
reachable_heap_size.self_count for a class that "should" only have
a handful of instances (Activities, Fragments, application singletons).root_type of ROOT_JAVA_FRAME or ROOT_JNI_GLOBAL retaining a lot
— these often indicate a native or JNI leak.Once a suspicious class is identified, the dominator tree tells you what is uniquely keeping its instances alive. An object's immediate dominator is the closest ancestor that every path from a GC root must pass through.
INCLUDE PERFETTO MODULE android.memory.heap_graph.dominator_tree;
WITH suspect_objects AS (
SELECT o.id
FROM heap_graph_object o
JOIN heap_graph_class c ON o.type_id = c.id
WHERE o.upid = $upid
AND o.graph_sample_ts = $ts
AND c.name = 'com.example.LeakySingleton'
)
SELECT
d.id,
d.idom_id,
d.dominated_obj_count,
d.dominated_size_bytes,
d.depth
FROM heap_graph_dominator_tree d
JOIN suspect_objects s USING (id)
ORDER BY dominated_size_bytes DESC;
To walk up from a leaked object to its GC root, follow idom_id
recursively (it's NULL once you hit the super-root). Join each step
through heap_graph_object.type_id → heap_graph_class.name to get a
human-readable retention path.
For a per-class rollup of the dominator tree (often easier to read than
per-object), use android.memory.heap_graph.dominator_class_tree
instead.
The dominator tree shows one retaining edge per object (the dominator).
If you need the full set of incoming/outgoing references — e.g. to
explain why an object can't be collected even though the dominator looks
benign — query heap_graph_reference directly. Note that
heap_graph_reference.owner_id is the source object id; you do not need
to join through reference_set_id.
-- Outgoing references from a specific object.
SELECT
r.field_name,
r.field_type_name,
oc.name AS owner_class,
r.owned_id AS referent_id,
rc.name AS referent_class,
ro.self_size AS referent_self_size
FROM heap_graph_reference r
JOIN heap_graph_object oo ON r.owner_id = oo.id
JOIN heap_graph_class oc ON oo.type_id = oc.id
LEFT JOIN heap_graph_object ro ON r.owned_id = ro.id
LEFT JOIN heap_graph_class rc ON ro.type_id = rc.id
WHERE r.owner_id = $object_id;
Common things to look for here:
ArrayList / HashMap / ConcurrentHashMap whose internal array
has thousands of slots — the leaked container is the suspect.WeakReference or SoftReference chain that turns out not to be
weak in practice (a strong reference is also held).Outer$Inner) with a this$0 field pinning an
Activity or Fragment — classic Android leak.A good summary for the user contains:
cumulative_size,
with class names, not raw IDs.Class -> field -> Class -> field -> ….Always keep the underlying SQL and object IDs available so the user can audit. Do not make claims about what the code is doing without inspecting the references.
android.memory.heap_graph.*):
https://perfetto.dev/docs/analysis/stdlib-docs