跪拜 Guibai
← All articles
Frontend · Flutter · Android

Flutter Impeller Vulkan Memory Leak: Shaky Shadow Cache Key and a Per-Frame Native Heap Tax

By 恋猫de小郭 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
— A Flame game with blurred HUD text shadows grew Native Heap from 34 MB to 775 MB in 27 minutes on Flutter 3.47.7 Impeller Vulkan.
— In controlled tests, a changing counter plus an unchanged label with blurRadius 4 grew Native Heap at 123 KB/s; the unchanged label alone grew at 8 KB/s.
— Flutter 3.47.7's TextShadowCache used TextFrame addresses as cache keys, so repeated identical text missed the cache and recreated shadow textures each frame.
— PR #190681 changes the cache key to a content fingerprint, fixing cross-frame cache hits for unchanged text.
— PR #194769 reuses temporary blur render targets, sets a 240 unused-frame limit and 8 MiB budget for the shadow cache, and reduces Mali-G68 Raster P50 from 5.35 ms to 1.94 ms.
— A CustomPainter that only executes save/clip/translate/scale/restore without drawing grows Native Heap at 10.1 KB/s on Release+Vulkan over 7 minutes; OpenGLES grows at 2.8 KB/s in Profile mode.
— Pausing the game loop and producing no frames brings growth to 0; Dart GC and dumpsys meminfo Graphics remain unchanged.
Conclusions

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.

Concepts & terms
TextShadowCache
A cache in Flutter's Impeller renderer that stores blurred text shadow textures keyed by some identifier of the text frame. In 3.47.7 it used the TextFrame address, causing misses when new TextFrame objects were created even for identical text.
Content fingerprint
A hash or digest derived from the actual content (characters, style, etc.) used as a cache key instead of object identity. PR #190681 replaces the address-based key with a content fingerprint so identical text reliably hits the cache across frames.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗