Repository navigation
Description
Summary
On an embedded ARM board (Rockchip RK3576, Mali-G52 MC3 Bifrost, proprietary ARM GLES driver "libmali" g24p0, vendor kernel 6.1 with bifrost_kbase, Debian 12, weston 10 kiosk-shell, GTK 3.24.38), a Flutter Linux app on Wayland grows its resident memory by roughly 5.7 KB per presented frame for as long as anything animates. A screen with one repeating AnimationController grows at ≈20 MB/min and reaches 5.7 GB in about four hours, after which the 8 GB board swaps for two hours and the kernel OOM-killer ends it. Leaving the animating screen stops the growth immediately.
The growth is not in the Dart heap. It is glibc main-arena memory allocated on the platform/UI thread by the GLES driver, one small set of bookkeeping objects per fl_framebuffer_create_sibling() + gdk_cairo_draw_from_gl() + delete round trip in fl_compositor_opengl_render(). The same app on x86_64 Mesa does not leak.
This is the same behaviour that #182192 (comment by richardexfo, 2026-02-26: embedded ARM + OpenGL ES, "any animation causes an increase in memory") and PR #183653 ("Linux reuse sibling") addressed; that fix was reverted in #183871 and the whole path was removed in #192150 in favour of FlViewRendererSubsurface. On this device the subsurface renderer (master f1ac3fe4, 3.48.0-1.0.pre-696) does not work either: the window is black, the first three launches SIGSEGV within 3 s, and the kernel logs ~1700 mali … Unhandled Page fault in AS1 at VA 0x0 / JOB_READ_FAULT lines. Devices with this class of driver currently have no working hardware-accelerated path on any channel.
Environment
| SoC / GPU | Rockchip RK3576, Mali-G52 3 cores r1p0 (0x07040002), DT compatible arm,mali-bifrost |
| Kernel | Linux 6.1.99 (vendor BSP), GPU kernel driver bifrost_kbase g25p0-00eac0 (UK 11.47); no panfrost/panthor |
| Userspace GPU | proprietary libmali-bifrost-g52-g24p0-x11-wayland-gbm 1.9 (provides libEGL, libGLESv2, libgbm, libwayland-egl) |
| Compositor | weston 10.0.1, kiosk-shell, drm-backend, single DSI output 1080×1240 |
| GTK / GLib | gtk3 3.24.38, glib 2.74.6, libepoxy 1.5.10, wayland 1.21.0 (Debian 12 bookworm, glibc 2.36) |
| Embedder env | GDK_BACKEND=wayland, GDK_GL=gles; FLUTTER_LINUX_RENDERER unset (OpenGL) |
| Flutter (leak) | 3.41.8 stable |
| Flutter (GPU fault) | master f1ac3fe4b34cbece23031cc78b864614aa5b5a43 (3.48.0-1.0.pre-696, Dart 3.14.0-214.0.dev, engine d062219657405470009c3884acd719c82ea0d42b) |
| App | release build of a full-screen GTK window; the app only presents frames, no platform views |
Part 1 — per-frame native leak (stable, OpenGL renderer)
Steps to reproduce
-
On the device above, run a release build of an app whose screen contains any repeating animation. A minimal one:
import 'package:flutter/material.dart'; void main() => runApp(const MaterialApp(home: Scaffold(body: Center(child: Spinner())))); class Spinner extends StatefulWidget { const Spinner({super.key}); @override State<Spinner> createState() => _SpinnerState(); } class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin { late final AnimationController _c = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))..repeat(); @override void dispose() { _c.dispose(); super.dispose(); } @override Widget build(BuildContext context) => AnimatedBuilder( animation: _c, builder: (_, __) => Container(width: 80, height: 14, color: Color.lerp(Colors.teal, Colors.white, _c.value)), ); }
-
Sample every 10 s:
P=$(pgrep -f my_app) awk '/VmRSS/{print $2}' /proc/$P/status awk '/\[heap\]/{f=1} f&&/^Rss:/{print $2; exit}' /proc/$P/smaps
-
Replace the screen with a static one (or stop the controller) and keep sampling.
Expected results
Resident memory settles after warm-up regardless of whether something animates.
Actual results
With exactly the sample above (release build, nothing else on screen), sampled every 20 s on the device:
09:49:22 rss=221992 kB [heap]=43044 kB anon=70276 kB threads=22
09:50:22 rss=249840 kB [heap]=69564 kB anon=97916 kB
09:51:23 rss=276940 kB [heap]=96456 kB anon=124044 kB
09:52:23 rss=310080 kB [heap]=128428 kB anon=157180 kB
09:53:23 rss=339252 kB [heap]=155388 kB anon=185848 kB
09:54:03 rss=356184 kB [heap]=171812 kB anon=202632 kB
[heap] +128.8 MB in 281 s ≈ 27.5 MB/min ≈ 7.6 KB per frame at 60 fps, linear, thread count constant. Process CPU averaged 82 % of one core (platform/UI thread 69 %, raster 12 %, driver's mali-cmar-backend 12 %) for an 80×14 px colour change.
The same growth, at ≈20 MB/min, is what took a production kiosk app (a screen with one repeating progress meter) to 5.7 GB in four hours. In that app, navigating to a static screen freezes [heap] to the byte within one sample and CPU drops to 0 %:
10:24:21 rss=727140 kB heap=303552 kB
10:26:23 rss=726400 kB heap=303552 kB (flat for the rest of the run)
What is leaking
Reading /proc/<pid>/mem over the newly grown [heap] range of the production app and walking glibc chunks (in-use = next chunk's PREV_INUSE), live growth over 30 s (≈10.7 MB) is:
| chunk size | live chunks added in 30 s | total |
|---|---|---|
| 832 B | +2119 | 1.7 MB |
| 131088 B (128 KB) | +13 | 1.6 MB |
| 240 B | +5964 | 1.4 MB |
| 16400 B | +35 | 0.6 MB |
| 176 B | +2317 | 0.4 MB |
| 32 B | +5555 | 0.2 MB |
Every 240 B object holds a pointer into libmali.so at the same offset (one object type); the 128 KB blocks hold libmali pointers too; libflutter_linux_gtk.so pointers are negligible. Over the process lifetime the 193 MB heap is 185 MB live, dominated by these three sizes (128 KB ×439, 832 B ×33455, 240 B ×90025). The only large freed chunk is 5,356,816 B = 1080×1240×4+16, i.e. the per-frame CPU readback buffer, which is reused, not leaked.
So the leaked memory is the GLES driver's bookkeeping for GL objects created and deleted on the platform thread once per frame. The embedder path (3.41.8 fl_compositor_opengl.cc, shareable == TRUE on Wayland):
draw_cb (GTK main thread)
→ fl_compositor_opengl_render()
→ fl_framebuffer_create_sibling() // glGenTextures + glEGLImageTargetTexture2DOES
// + glGenFramebuffers + glFramebufferTexture2D
→ gdk_cairo_draw_from_gl(texture) // GTK3 falls back to glReadPixels + cairo here
→ g_autoptr releases the sibling // glDeleteTextures / glDeleteFramebuffers
The same build on an x86_64 Mesa (radeonsi) desktop presents at 60 fps through the same code and its [heap] stays flat. With FLUTTER_LINUX_RENDERER=software on the ARM device the growth disappears entirely (RSS flat at 273 MB over 3 min on a video screen that grows ≈5 MB/min under OpenGL), which matches richardexfo's observation in #182192 — but the software renderer has no external-texture callback, so Texture widgets render nothing, and CPU rasterising a 1240×1080 panel costs ~14 % of a core at 1 fps.
PR #183653 kept the sibling alive until the next frame instead of deleting it right after gdk_cairo_draw_from_gl, and its author reported that #183539 (EGLImage GObject) alone did not stop this leak. That matches what we see: it is the delete-while-still-referenced pattern per frame, not the EGLImage lifetime.
Part 2 — master's FlViewRendererSubsurface faults the GPU on the same device
Steps to reproduce
- Build the same app with master
f1ac3fe4(contains Add FlViewRendererSubsurface #191389, Fix black window in the subsurface view renderer #191850, [Linux] Use fences to synchronize frames between OpenGL contexts #192098, [Linux] Remove frame sharing between OpenGL contexts #192150). The engine selectsFlViewRendererSubsurfacefor every Wayland display; there is no way to chooseFlViewRendererOpenGL. - Install and start under weston 10 kiosk-shell.
- Watch
journalctl -k -f | grep gpuand take a compositor screenshot (weston-screenshooter).
Expected results
The window renders as it does with 3.41.8.
Actual results
- Launches 1–3:
Main process exited, code=killed, status=11/SEGVabout 3 s after start (systemd restarts it). - Launch 4 onwards: process stays alive (21 threads, RSS 257 MB, 0 % CPU) but the screen is black. No output at all from the app or GTK, even with
G_MESSAGES_DEBUG=all. - Kernel log, ~1700 lines within 2 s of the surviving instance starting (none before that day):
mali 27800000.gpu: Unhandled Page fault in AS1 at VA 0x0000000000000000
Reason: Memory is not mapped on the GPU
mali 27800000.gpu: error detected from slot 1, job status 0x00000004 (TERMINATED)
mali 27800000.gpu: t6xx: GPU fault 0x04 from job slot 1
mali 27800000.gpu: t6xx: GPU fault 0x04 from job slot 0
mali 27800000.gpu: error detected from slot 1, job status 0x00000042 (JOB_READ_FAULT)
mali 27800000.gpu: t6xx: GPU fault 0x42 from job slot 1
mali 27800000.gpu: t6xx: GPU fault 0x42 from job slot 0
(repeats)
AS1 is the second GPU address space, i.e. the Flutter process (weston holds AS0). The jobs read through a null/unmapped descriptor on both vertex and fragment slots. weston's log shows no client or protocol error. Downgrading to the 3.41.8 build restores the display immediately.
libwayland-egl.so.1 resolves to the driver's own copy; wl_egl_window_create and the shared EGL context are new in this path compared with the OpenGL renderer, so the fault is somewhere in "engine EGL context shared with a second context + wl_egl_window surface" on this driver. We have no user-space backtrace yet (no gdb on the device); we can add one if it helps.
Why this matters
Boards with ARM's proprietary Bifrost/Valhall GLES driver are common in kiosk/HMI products. On them:
- every stable from 3.38 to 3.47 leaks per frame on Wayland through the sibling path;
- master replaces that path with one that faults the GPU;
- the only leak-free option,
FLUTTER_LINUX_RENDERER=software, dropsTexturesupport and burns CPU.
A build that keeps FlViewRendererOpenGL available on Wayland (or an env switch) together with the deferred sibling release from #183653 would give these devices a working path while the subsurface renderer is sorted out. Happy to test patches on this hardware and to provide a user-space backtrace or apitrace capture for either part.
Related
#182192, #183539, #183653, #183871, #184028, #192150, #191389, #191850, #192098, #73402 (2026-08-31 comment: Pi 5 / Mesa / X11 +4.4 MB/h at 1 Hz), #152099 / #166753 (ARM driver and glBlitFramebuffer).
Flutter Doctor output
flutter doctor is not available on the device (no SDK there); the builds come from an arm64 Linux CI runner. Versions from the build logs:
Flutter 3.41.8 • channel stable (Part 1)
Flutter 3.48.0-1.0.pre-696 • channel [user-branch] • master f1ac3fe4
Engine d062219657405470009c3884acd719c82ea0d42b • Dart 3.14.0-214.0.dev (Part 2)
Activity
suryaKommana2662 commentedon Sep 11, 2026
@ZhoufangErqiangu Thanks for the investigation and reproduction data. I noticed that the report was tested on Flutter 3.41.8 stable and a master/pre-release build. Could you please verify whether the issue is still reproducible on the latest stable Flutter release and provide the Flutter version used for that verification
ZhoufangErqiangu commentedon Sep 11, 2026
Verified on the latest stable today, same board, same minimal sample as above (release builds from a Linux arm64 CI runner,
flutter precache --linuxengine artifacts):06a2e2a110089dff50fe635cffd2a61e1b24fbcd5a2a6a42cce67f965cf540fcecf616faca624aa1[heap]47.8 MB → 177.5 MB in 281 s ≈ 27.7 MB/min, thread count constant (22), process CPU 82 % (UI/platform thread 75 %, raster 12 %)The 3.47.3 crash is on the raster thread inside the GLES driver, with
debug.exception-traceenabled:systemd:
Main process exited, code=killed, status=11/SEGV×10 within 45 s, then the start limit stops it. No kernel GPU faults this time (unlike the master build), nothing on stderr even withG_MESSAGES_DEBUG=all. Rolling back to the 3.44.9/3.41.8 build restores the display immediately.engine/src/flutter/shell/platform/linux/fl_framebuffer.cc,fl_compositor_opengl.ccandfl_opengl_manager.cchave no GL-format changes between 3.44.5 and 3.47.3, so the regression is elsewhere in the raster path; I can capture a user-space backtrace (gdb on the device) if that would help narrow it.So on this driver: 3.41–3.44 leak per frame at the same rate through the sibling path (which is unchanged in 3.47.3:
fl_compositor_opengl.cclines 274–295 still create and delete the sibling texture every frame), 3.47.3 does not start, and master faults the GPU in the subsurface rendererrobert-ancell commentedon Oct 16, 2026
@ZhoufangErqiangu can you try on master with Impeller disabled by setting
fl_dart_project_set_enable_impeller(project, FALSE);inlinux/runner/my_application.cc?ZhoufangErqiangu commentedon Oct 16, 2026
@robert-ancell Tried it on our own RK3576 bench (same Mali-G52 Bifrost / libmali g24p0-00eac0 family as the report). Built master (framework
132e1c65bc, engine5db6dbc8f3d4dd0de9d49f396310ea01af5d2bd5) withfl_dart_project_set_enable_impeller(project, FALSE)inmy_application.cc.No crash, no GPU fault. Our full kiosk app ran clean under weston kiosk-shell — no SIGSEGV,
dmesghad zeroJOB_READ_FAULT/page-fault lines,NRestarts=0, and the compositor screenshot shows real rendered UI, not black. So the fault does look like it's coming from the Impeller backend, not theFlViewRendererSubsurfacepresentation path itself.And the leak is gone too. I re-ran the exact minimal repro from this issue (the
Spinner/AnimationController.repeat()snippet) with the same build, samplingVmRSS/[heap]every 20 s for 5 minutes the same way:Flat —
[heap]moved by ≤4 kB over the whole 5 minutes, versus +128.8 MB/281s (≈27.5 MB/min) for the identical repro on 3.41.8 stable reported above.top -Hduring the run showedio.flutter.rasterandmali-cmar-backendcontinuously busy (not idle), so this isn't just a case of the animation silently not running — frames were being submitted and presented throughout.So on this driver it looks like disabling Impeller gets you both: no GPU fault and the per-frame leak is fixed (makes sense — the subsurface renderer doesn't do the create/delete-sibling-texture dance that was the original leak source).
One separate thing I noticed, possibly unrelated: in both tests the GTK window didn't fill the 1080×1240 output on master — content was confined to a sub-region with black filling the rest. Haven't root-caused it yet (window/surface sizing against the compositor, not a crash), flagging in case it's a known regression.
Happy to pull a fuller trace (
WAYLAND_DEBUG, apitrace) if useful — let me know what would help narrow down the Impeller-specific fault.