Flutter Impeller Vulkan Memory Leak: Shaky Shadow Cache Key and a Per-Frame Native Heap Tax
Flutter apps on Android that keep drawing frames will see native memory climb persistently under Impeller Vulkan, and the shadow-cache fix removes only part of the leak. Upstream engineers still need to explain the empty-frame growth before teams can trust long-running Vulkan sessions.
Starting from a Flame game where HUD text shadows pushed Native Heap from 34 MB to 775 MB in 27 minutes, the investigation narrowed a large part of the growth to a broken cache key in Impeller's TextShadowCache. Flutter 3.47.7 keyed the cache on the address of TextFrame, so identical glyphs across frames missed the cache whenever a new TextFrame was allocated. The frame-end cleanup then deleted unused entries, forcing repeated Gaussian Blur texture generation. Two PRs address this: #190681 replaces the cache key with a content fingerprint, and #194769 reuses temporary render targets and caps the shadow cache at 240 unused frames and 8 MiB, cutting Mali-G68 Raster P50 from 5.35 ms to 1.94 ms.
The deeper problem persists. A CustomPainter that only sets transform and clip state, drawing nothing, still grows Native Heap at 10.1 KB/s on Release+Vulkan over 7 minutes. Profile mode shows 13.0 KB/s on Vulkan versus 2.8 KB/s on OpenGLES. At 60Hz that is roughly 170 bytes per frame. Dart GC does not reclaim it, and dumpsys meminfo shows no change in Graphics. The Vulkan backend or the driver's allocator appears to retain memory in a way OpenGLES does not.
Vulkan-only empty-frame growth points to Impeller's resource lifecycle or driver allocator retaining memory, not C++ object leaks in the app.
Using an object address as a cache key is a classic pitfall; the fingerprint fix is correct, but the budget in #194769 is equally important to prevent cache churn from recreating textures.
A 5.35 ms to 1.94 ms Raster P50 improvement on Mali-G68 shows the cache miss was costing real GPU time, not just memory, so the fix likely improves frame pacing on that GPU.