mapbox / mapbox/mapbox-gl-js

Raster-array timeline playback: visible-tile decoded band ArrayBuffers accumulate in JS heap; memory does not drop after pause; partial tile eviction ineffective

Open
#13,688 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

auto-triaged bug :lady_beetle:
Dominant language
TypeScript
Stars
12.4k
Forks
2.4k
PR merge metrics
No merged PRs in 30d

Description

# Mapbox Support Issue — Raster-array timeline playback retains visible-tile band ArrayBuffers (JS heap / process memory growth)

> **Purpose:** Copy/paste sections below when opening a ticket with Mapbox Support or filing a GitHub issue on `mapbox/mapbox-gl-js`.
>
> **Product:** FunWaves marine weather map (wind / wave height overlays on Mapbox GL JS inside mobile WebView)

---

### Title

**Raster-array timeline playback: visible-tile decoded band ArrayBuffers accumulate in JS heap; memory does not drop after pause; partial tile eviction ineffective**

### Summary

We use **Mapbox GL JS** with **`raster-array`** sources (MTS tilesets) to animate wind/wave fields over a timeline by updating `raster-array-band` on heatmap + `raster-particle` layers (~1 band every 900 ms).

During playback, **main-thread JS heap** (`performance.memory.usedJSHeapSize`) grows monotonically (e.g. **~150 MB → 500 MB → 1 GB+**). **Pausing playback does not cause memory to fall back.** Chrome DevTools heap snapshots show **`ArrayBuffer` as the largest retained type**.

We attempted to release memory via **partial source-cache eviction** (removing off-screen / non-current-zoom tiles only). This **does not reduce heap**, which matches Mapbox Native SDK documentation that **`tileCacheBudget` does not account for resources allocated by visible tiles** ([Android RasterArraySource.tileCacheBudget](https://docs.mapbox.com/android/maps/api/11.2.1/mapbox-maps-android/com.mapbox.maps.extension.style.sources.generated/-raster-array-source/tile-cache-budget.html)).

On GL JS, inspecting the bundle shows each raster-array tile keeps an MRT decoded-band LRU with **`MRT_DECODED_BAND_CACHE_SIZE = 30`** when `partial: true` (default), plus possible `entireBuffer` on the main thread ([`raster_array_tile_worker_source.ts`](https://github.com/mapbox/mapbox-gl-js/blob/main/src/source/raster_array_tile_worker_source.ts), [`raster_array_tile.ts`](https://github.com/mapbox/mapbox-gl-js/blob/main/src/source/raster_array_tile.ts)).

With ~9 visible tiles at zoom 5, timeline playback appears to retain a large number of decoded band blocks across visible tiles, leading to **GB-scale heap retention** and **mobile WebView OOM-kill risk**.

Separately, **Cache Storage** (`mapbox-tiles`) accumulates many `.mrt?range=bytes=...` entries (expected for partial/range fetching). This appears to be **disk cache** (Mapbox enforces ~500 entries) and is **not** the primary driver of JS heap growth, but we mention it for completeness.

### Environment

| Item | Value |
| --- | --- |
| SDK | **mapbox-gl-js ^3.20.0** (resolved **3.24.x** in lockfile during investigation) |
| Host | Next.js 14 web app, also embedded in **React Native WebView** (iOS / Android) |
| Basemap | `mapbox://styles/mapbox/dark-v11` |
| Source type | `raster-array` (MTS), `tileSize: 512`, `minzoom: 3`, `maxzoom: 8` |
| Layers | `raster` (heatmap) + `raster-particle` on same source |
| Band animation | `map.setPaintProperty(layer, 'raster-array-band', bandId)` every **~900 ms** |
| Map options | `maxTileCacheSize: 16` |
| Example tile URL pattern | `/rasterarrays/v1/funwaves.wind-openmeteo-dev/{z}/{x}/{y}.mrt?jobid=...&range=bytes=...` |
| Browser (repro) | Chrome desktop (DevTools Memory / Application panels) |

### Steps to reproduce

1. Load a map with a `raster-array` source and layers styled with `raster-array-band`.
2. Ensure **~6–12 tiles** are visible (e.g. zoom 4–6 over Japan).
3. Start **timeline playback**, advancing bands programmatically every ~900 ms (do not pan/zoom during playback).
4. Observe `performance.memory.usedJSHeapSize` (or Performance Monitor JS heap) over 2–5 minutes.
5. **Pause playback** and wait several minutes (optionally trigger manual GC in DevTools).
6. (Optional) Attempt partial eviction by removing off-screen tiles from internal source cache (equivalent to evicting non-visible tiles while keeping viewport tiles).
7. (Optional) Take heap snapshots before playback, after playback, and after pause.

### Expected behavior

- Decoded band data for **previously displayed bands** on **visible tiles** should be reclaimed after bands are no longer needed (LRU bounded memory per tile, or explicit release when band paint property changes).
- Pausing playback should allow memory to stabilize or decrease.
- Documented cache budget / eviction mechanisms should prevent unbounded growth during band animation.

### Actual behavior

- JS heap **monotonically increases** during playback.
- **Pause does not restore** heap to baseline (not merely GC lag — retained size stays high).
- Heap snapshot: **`ArrayBuffer`** dominates retained size.
- Partial eviction of **off-screen** tiles: **`removedTiles: 0`**, heap **unchanged** (visible tile count unchanged, e.g. 9/9).
- Full `clearTiles()` on the active source **does** force reload and is the only approach we found that can release visible-tile resources — but causes visible flicker, which we are trying to avoid during playback.

### Measurements (representative)

| Metric | Before playback | During playback | After pause |
| --- | --- | --- | --- |
| JS heap (`usedJSHeapSize`) | ~150 MB | ~500 MB – **1069 MB** | **No meaningful drop** |
| Visible tiles | ~6–9 | ~6–9 | ~6–9 |
| `activeOffscreenTiles` | 0 | 0 | 0 |
| `pressure` (custom) | none | none | none |
| Partial evict result | — | `z5 -0t -0c` | same |

### What we tried (workarounds)

| Action | Effect on JS heap |
| --- | --- |
| Partial eviction (`touchActiveOffscreen: true`, evict off-screen tiles) | **No decrease** |
| Partial eviction (gentle, non-current zoom only) | **No decrease** |
| Pause playback | **No decrease** |
| `clearTiles()` / full source cache clear | Releases memory but **full viewport reload / flicker** |
| Lower `maxTileCacheSize` (16) | Tile count bounded, but **band ArrayBuffer growth continues** on visible tiles |

### Questions for Mapbox

1. **Is monotonic growth of decoded band `ArrayBuffer`s on visible tiles during `raster-array-band` animation expected** when `partial: true` and MRT cache size is 30?
2. Is there a **supported GL JS API** (style property or runtime call) equivalent to Native **`RasterArraySource.tileCacheBudget`** or **`MapboxMap.reduceMemoryUse()`** for raster-array **band** memory on **visible** tiles?
3. Should we use **`volatile: true`** (Native experimental) or another source flag to limit in-memory band retention? (Style spec notes GL JS does not yet support `volatile`.)
4. Is there a recommended pattern for **time-series raster-array animation** that avoids retaining all previously visited bands in visible-tile MRT caches?
5. Does Mapbox consider **`clearTiles()` during pause / memory pressure** the intended recovery path for this scenario?

### References

- Native `tileCacheBudget` caveat (visible tiles excluded):
https://docs.mapbox.com/android/maps/api/11.2.1/mapbox-maps-android/com.mapbox.maps.extension.style.sources.generated/-raster-array-source/tile-cache-budget.html
- Native `reduceMemoryUse()` (clears in-memory tile/image/texture caches):
https://github.com/mapbox/mapbox-maps-ios/blob/main/Sources/MapboxMaps/Foundation/MapboxMap.swift
- GL JS raster-array source (`partial` default true):
https://docs.mapbox.com/mapbox-gl-js/api/sources/#rasterarraytilesource
- GL JS `maxTileCacheSize`:
https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters
- Style spec `raster-array`:
https://docs.mapbox.com/style-spec/reference/sources/#raster-array

### Contact / project context

- **App:**(marine weather map for surfers)
- **Use case:** Animated wind/wave heatmap + particles over ~24 hourly bands
- **Priority:** Mobile WebView stability (OOM kills on mid-range devices)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/source/raster_array_tile_worker_source.ts and src/source/raster_array_tile.ts, especially the partial decoded-band cache described in the report. Reproduce playback with 6–12 visible tiles, then compare heap snapshots during playback and after pause or clearTiles(). Done means the visible-tile band buffers are bounded or released without requiring a full viewport reload, with behavior covered by an appropriate regression test.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
frontend, performance, web-dev
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.