grow_job_pool — minimal reproduction for flutter/flutter#189190
Minimal Flutter app that reproduces a null-pointer SIGSEGV inside the
MediaTek PowerVR GL driver (libGLESv2_mtk.so, glClear) on the Impeller
OpenGLES backend. The crash is caused by the driver's fixed-size internal
job pool being exhausted: an allocation failure returns null and the
driver dereferences it without checking.
Related: PR flutter/flutter#189643
The single screen requires no interaction. It shows a grid of 20 small animated image tiles (10 GIF + 10 WebP), each wrapped in a rounded clip. Two properties of that layout — distilled from the affected app's image widget — are what fill the driver's job pool faster than the driver retires jobs while the GPU is otherwise idle:
- One offscreen render pass per tile per frame. A rounded
ClipRRectpushes Impeller onto asaveLayer, so each tile gets its own offscreen render target. On a tile-based PowerVR GPU every offscreen FBO is a separate pair of driver jobs (TA + 3D), and the animated content keeps them being submitted every frame. - A steady stream of decode/upload jobs. The assets are byte-identical
duplicates under distinct paths (
g1..g10.gif,w1..w10.webp). Flutter'sImageCacheis keyed by provider, so reusing one path N times would still yield a single decode loop — the duplicated files are what actually multiply the number of decode/upload jobs per second.
All tiles are laid out with Wrap so they stay inside the viewport: animated
images only keep decoding while they have listeners, so anything scrolled
off-screen would stop contributing jobs. For the same reason the app must stay
in the foreground — backgrounding it stops frame production, the driver
drains the pool, and the clock effectively resets.
GL memory stays flat at roughly 45 MB (EGL mtrack ~19 MB + GL mtrack ~26 MB, total PSS ~110 MB) across the whole session, including sessions that exhausted the pool 29 times over an hour. This is job-pool pressure, not memory pressure.
Both the IO thread and the raster thread allocate from the same pool, but the consequences differ — this is why the same driver bug shows up as either a harmless log line or a hard crash:
1.iosubmits texture transfer jobs (TQMQueueMipgen/TQBlitTextureLevelvia the TDM transfer queue). When allocation fails here the driver drops that upload and carries on — rendering continues and the app survives. Every burst below marked "io" is of this kind.1.rastersubmits the render pass. When allocation fails here, the returned null reachesglClear, which dereferences it → SIGSEGV.
Under moderate load the IO thread submits far more often than the raster thread, so it is almost always the one that hits the ceiling first, and the app keeps running. Raising the tile count raises the raster thread's submission density until it can land inside the exhaustion window too.
Consequence for triaging: RM_GrowJobPool alone does not predict a crash.
Grep the tid — the precursor on 1.io is survivable, on 1.raster it is
fatal. A session logged 29 io-side precursors over 77 minutes without
crashing; another crashed 3 ms after its first raster-side one.
Use a stock Flutter (no custom engine); the mitigation in #189643 would suppress the crash. Impeller OpenGLES must be the active backend (the default on MediaTek devices, which avoid Vulkan).
flutter run --release
# or
flutter build apk --release && adb install -r build/app/outputs/flutter-apk/app-release.apkThen leave the app in the foreground and keep the screen on (Settings → Developer options → Stay awake while charging). Make sure nothing else takes focus — any app that comes to the front pauses the repro.
Watch logcat for the driver precursor and the crash. Keep the tid column: it is what tells a survivable failure from a fatal one.
adb logcat | grep -E "RM_GrowJobPool|Failed to allocate Job|TQMQueueMipgen|Fatal signal|libGLESv2_mtk"Confirm the OpenGLES backend is in use:
adb logcat -d | grep "Using the Impeller rendering backend"
# -> "...(OpenGLES)."Repeats on a ~120–170 s cadence for as long as the app is in the foreground. The app keeps rendering normally through these:
E IMGSRV : :1136: RM_GrowJobPool: Number of job pool chunks in use is too large to allow growth
E IMGSRV : :1197: Failed to grow job pool
E IMGSRV : :2800: RM_InsertJob_NoLock: Failed to allocate Job
E IMGSRV : :2207: TQMQueueMipgen: Failed to create sync command
E IMGSRV : :950: MakeMipmapLevelsForOneLayerTQ: TDM mipgen failed
E IMGSRV : :1136: RM_GrowJobPool: ...
E IMGSRV : :707: TQMTDMQueueTransfer: Failed to create sync command
E IMGSRV : :1020: TQBlitTextureLevel: Transfer queue function failed
The raster thread hits the same exhausted pool a few milliseconds after the IO
thread and dies immediately. Observed sequence (tids: 20724 = 1.io,
20722 = 1.raster):
17:31:04.032 20724 E IMGSRV : RM_GrowJobPool / Failed to grow / Failed to allocate Job
17:31:04.033 20724 E IMGSRV : TQMQueueMipgen: Failed to create sync command
17:31:04.034 20724 E IMGSRV : TQBlitTextureLevel: Transfer queue function failed
17:31:04.035 20722 E IMGSRV : RM_GrowJobPool / Failed to grow / Failed to allocate Job
17:31:04.035 20722 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR),
fault addr 0x0 in tid 20722 (1.raster)
Tombstone:
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000000
Cause: null pointer dereference
pid: 20056, tid: 20722, name: 1.raster
Process uptime: 214s
#00 pc 000000000019e794 /vendor/lib64/egl/libGLESv2_mtk.so
#01 pc 00000000000f740c /vendor/lib64/egl/libGLESv2_mtk.so
#02 pc 00000000000afab4 /vendor/lib64/egl/libGLESv2_mtk.so
#03 pc 00000000000b0150 /vendor/lib64/egl/libGLESv2_mtk.so (glClear+564)
#04..#16 libflutter.so (Impeller raster submission path)
#17 android::Looper::pollInner(int)+1068
...
#23 __pthread_start(void*)+208
x1, x8, x20, x25 are all 0 — the failed allocation's null return used
as a pointer.
All on the device below, same stock engine, varying only _gifCount /
_webpCount. "First precursor" is measured from ActivityManager: Start proc.
| tiles | time to crash | uptime | first precursor | precursor bursts | outcome |
|---|---|---|---|---|---|
| 12 | 4m11s | 4m11s | (none logged) | 0 | crash (1.raster, glClear) |
| 12 | — | 17m01s | T+140.5 s | 6 (all io) | killed by reinstall |
| 16 | — | 1h17m14s | T+123.3 s | 29 (all io) | killed manually |
| 20 | 3m34s | 3m34s | T+116.2 s | 1 (io, then raster) | crash 3 ms after first burst |
Time from launch to crash was 4m11s and 3m34s in the two sessions that crashed. That is much faster than the 10–100 minutes seen in the app this was distilled from, because this demo deliberately concentrates the pressure — the real app has far fewer animated images on screen at once. The two sessions with a dash never crashed; they were stopped by hand, so their uptime is a lower bound on how long a session can survive, not a time-to-crash.
Two more things to read from this:
- First precursor scales sub-linearly with tile count (140.5 → 123.3 → 116.2 s for 12 → 16 → 20 tiles). Adding tiles brings the ceiling closer but with clearly diminishing returns, so cranking the count is not a shortcut to an instant repro.
- Time-to-crash is not predictable from the precursors. The first session crashed at 4m11s having logged no precursor at all (logcat covered the whole session), while the 16-tile one logged 29 and never crashed. The crash needs the raster thread to land inside an exhaustion window, which is a race.
Each configuration was run once (n=1 per row), so treat the individual timings as indicative rather than reproducible constants.
| Device | OPPO CPH2203 |
| SoC | MediaTek MT6779 (Helio P90) |
| GPU | Imagination PowerVR Rogue GM9446 |
| GL driver | libGLESv2_mtk.so (BuildId 5f0230c5c21f5c7f1d1c30e576a5e1fd) |
| GL version string | OpenGL ES 3.2 build 1.13@5776728 |
| Android | 13 (API 33) |
| Build fingerprint | OPPO/CPH2203/OP4F43L1:13/TP1A.220905.001/R.236c2bc+24:user/release-keys |
Flutter 3.44.8 • channel stable
Framework • revision 058e0af2c2 • 2026-07-23
Engine • hash 13ffd72b2f9a5ca4db2a74ea52d5353ec2e8f939 (revision 0cd610717b) • 2026-07-23
Tools • Dart 3.12.2 • DevTools 2.57.0
Built --release, no engine patches, no --enable-impeller / backend flags —
the device selects Impeller OpenGLES on its own (it logs a Vulkan attempt
first, then falls back).
adb shell getprop ro.product.model
adb shell getprop ro.board.platform
adb shell getprop ro.vendor.mediatek.platform
adb shell getprop ro.build.fingerprint
adb shell dumpsys SurfaceFlinger | grep -i "GLES:" # GPU / GL driver stringIf a device reproduces the RM_GrowJobPool precursor, that log line is the
discriminator that distinguishes this job-pool-exhaustion crash from the
context-management crashes tracked in #187404 / #166248 (which share the same
libGLESv2_mtk.so SIGSEGV signature but a different mechanism).
The knobs are top-level constants in lib/main.dart:
_gifCount/_webpCount(currently 10 each) — more tiles means more offscreen passes and more decode loops per frame, filling the pool faster and, more importantly, raising the raster thread's odds of being the one that hits the exhausted pool.assets/ships 10 of each; going higher means duplicating more copies under new paths (each path needs its own file — same bytes is fine)._tileSize(default 88.0) — larger offscreen render targets per tile.