Playback growth¶
The problem¶
A media app's memory climbs while it plays, or after repeated starts and stops, and settles somewhere higher than where it began. A single dump cannot say what grew. You need a timeline, and a way to tell Java-heap growth from native growth, and then a way from a growing class to the object that owns it.
The solution¶
-
Capture a series and the native totals. Take three dumps while you play, and the meminfo straight after. Add
--auto-mappingif you have a release build, so the transcript records which mapping file belongs to the dumps. -
Read the timeline. Rename the dumps after their state, then run:
heaptrail --diff-series launch.hprof play.hprof soak.hprof --diff-by bytes --top 8 --native-context meminfo.txtMonotonic growth candidates (top 8): +336232 bytes 6951 count int[] +205200 bytes 5130 count p1.j +82560 bytes 5160 count s1.l +62720 bytes 2240 count char[] +58552 bytes 563 count d1.q2 +47208 bytes 1967 count java.lang.String +46144 bytes 824 count z1.z +38688 bytes 2418 count f1.g Native context (dumpsys meminfo, PSS KiB): Java Heap: 43152 KiB Native Heap: 88852 KiB Graphics: 0 KiB TOTAL: 186892 KiB warning: missing meminfo row: GLTwo things matter. The monotonic growth candidates at the top are your shortlist. The native context at the bottom shows that the Java heap is about 42 MiB and native about 87 MiB, so the Java heap is not the whole story.
-
Restore names. If the candidates read like
p1.j, run again with--mapping(or--auto-mapping). The shortlist then names real classes. The mapping file in these examples is a small demonstration file with invented class names. See Deobfuscate release builds. -
Trace the candidate in the last dump. Take a class from the list and ask who holds it. Targets are matched against the names in the dump, so give the name as the dump prints it:
heaptrail -i soak.hprof --find-referrers p1.j --mapping myapp/app/build/outputs/mapping/universalRelease/mapping.txt --hops 1 --top 4Found 11382 target instance(s) for com.example.myapp.player.PlaybackState === Direct referrers (1-hop) === holder.field (or class[] for arrays) ref count p1.b.j 1238 p1.b.a 1238 p1.b0.j 276 p1.b0.a 276The target is restored to
PlaybackStatein the first line. The holder rows come back asp1.b.j,p1.b.a,p1.b0.j: more obfuscated names. Add the mapping for those classes too, or trace further with--paths-from-id. For a family of classes in one package, collapse the table with--target-glob 'androidx.media3.**' --hops 2 --group-holders. See Group holders. -
Confirm it is held, not garbage. Use
--leak-suspects --exclude-soft-weakon the last dump. A class that grows and ranks as a suspect is held. A class that grows but is unreachable is allocation churn. See Leak suspects.
How it works¶
The series isolates the classes that only grow, the meminfo says whether the Java heap is where the growth lives, and the tracing modes name the owner. Each step narrows the question: which classes, which memory, which holder.
Variations¶
- Only two dumps. Use
--diff-fromand--diff-to. With two you cannot tell a steady climb from a single jump. - Native memory is the growth. If the Java heap is flat and native grows, no dump will find it. Look at Graphics and the native heap in meminfo, check bitmaps, and use a native profiler.
- Allocation stacks. If a class grows and you need the code that creates it, capture one more dump with allocation tracking.
- Compose and view-heavy apps. Candidates such as snapshot sets, slot tables and semantics nodes often rank high after many recompositions. Check whether they are still reachable before you treat them as a leak.
Related¶
- Diff a series explains each part of the report.
- Capture a series takes the dumps.
- Android for names in release builds.