Skip to content

TileImage never releases its decoded ImageInfo, leaking ~4 MiB per pruned tile #2265

Description

@bandy87

What is the bug?

Is there an existing issue for this?

Searched; #2006 is related but was closed as not planned and concerns a
different symptom. PR #2239 proposes the fix this report confirms.

Current Behaviour

On a physical device, rapid pinch in/out zooming makes memory grow without
bound until the OS kills the app. Memory that is not GPU texture memory
grows monotonically while the graphics category stays flat, which points at
decoded image bytes that are never freed rather than at texture pressure.

Expected Behaviour

Tiles that are pruned or replaced release their decoded image, and memory
stays flat during sustained zooming.

Root cause

ImageStreamCompleter.setImage hands every listener its own image.clone()
(painting/image_stream.dart:736-740), and RawImage clones again for its
RenderImage (widgets/basic.dart:6997). Handing the image to the widget
tree therefore never transfers ownership: the handle belongs to TileImage.

TileImage releases it nowhere:

  • _onImageLoadSuccess (tile_image.dart:159-167) overwrites imageInfo
    without disposing the previous frame;
  • dispose() (:216-245) removes the stream listener but never calls
    imageInfo?.dispose().

Every tile that is loaded and later pruned therefore leaks its decoded bytes
for the lifetime of the process — 4 MiB for a 512px tile at @2x
(1024x1024, RGBA8).

There is a third path: setImage iterates over a copy of the listener list,
so a listener removed in dispose() can still receive a frame. That image is
never painted and never owned — it leaks too.

How can we reproduce it?

Steps To Reproduce

  1. TileLayer with 512px retina tiles (tileDimension: 512, @2x URL).
  2. On a physical device, pinch-zoom in and out rapidly and continuously.
  3. Sample adb shell dumpsys meminfo <pid> every 5 s.

Measurements

Samsung SM S938B (Android 16, dpr 3), Flutter 3.44.6, release build,
flutter_map 8.3.2. Continuous rapid pinch zooming, values in MB.

Before the fix:

t Graphics Private Other TOTAL PSS
5-60 197-475 34-60 347-651
70 s 475 1129 1876
85 s 475 3230 4243
95 s 475 3387 7456
100s app killed by the OS

Note the shape: Graphics freezes while Private Other explodes. In an
earlier run the peak was 6025 MB of Private Other.

After applying PR #2239's fix (plus the disposed-listener case above):

t Graphics Private Other TOTAL PSS
5-130s 201-627 34-77 361-900

Stable for 130 s of continuous zooming, no growth trend, no kill. Graphics
now rises and falls with zoom levels instead of freezing.

Hypotheses ruled out by measurement

  • Smaller tiles (dropping @2x): memory halved, but Graphics rose and
    the app died sooner (~35 s vs ~50 s). The retain rule counts tiles, not
    bytes, so smaller tiles simply let more be retained.
  • Built-in disk cache: with DisabledMapCachingProvider the explosion is
    unchanged and Native Heap stays low, so the cache writer isolate is not
    involved.
  • Forcing a layer reset (via TileLayer.reset) after each zoom: far worse
    — the app died in ~20 s. removeAll goes through the same leaking
    dispose() and forces a full reload, multiplying the leak.
  • Upgrading 8.3.1 -> 8.3.2: no effect. (Pinch-zoom produces a NaN camera zoom when a finger is lifted mid-gesture, freezing the map (ANR + OOM kill) #2244 was closed as a duplicate of
    Gestures with very close start/end points causes division by zero #2221, but the NaN path it describes is still present in 8.3.2.)
  • Reducing tile creation (panBuffer: 0, keepBuffer: 1, throttled
    tileUpdateTransformer): helps a lot — the stable window grew from ~10 s to
    60 s — but only lowers the slope, confirming the leak itself is untouched.

Version

flutter_map 8.3.2 (also present on master as of 2026-09-04), Flutter 3.44.6
stable, Dart 3.12.2, Android 16 release build.

Do you have a potential solution?

Yes — PR #2239 already implements this fix, and I can confirm it works.

I applied its changes on top of v8.3.2 and measured the result on a physical device: memory stays flat (Private Other 34-77 MB) through 130+ seconds of continuous rapid zooming, where the unpatched build was killed by the OS after ~50 seconds.

One detail worth highlighting, because it is in the PR's code but not in its description: besides releasing the handle on frame replacement and in dispose(), the third case matters in practice — setImage iterates over a copy of the listener list, so a listener removed in dispose() can still receive a frame. That image is never painted and never owned, so it must be released in place. With panBuffer: 0 and fast pinch gestures this path is hit regularly, not rarely.

The PR's CI is currently red only on "Analyse Code", which fails at dart format --set-exit-if-changed — the test jobs pass on both Flutter channels.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs triageThis new bug report needs reproducing and prioritizing

    Type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions