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,685 2 comments 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-gl-js version

mapbox-gl-js ^3.20.0 (resolved 3.24.x in lockfile during investigation)

### Browser and version

_No response_

### Expected behavior

# 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)

### Actual behavior

_No response_

### Link to the demonstration

_No response_

### Steps to trigger the unexpected behavior

_No response_

### Relevant log output

```shell

```

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 by reproducing the timeline playback steps and inspecting src/source/raster_array_tile_worker_source.ts and src/source/raster_array_tile.ts, especially the partial-band cache behavior described in the issue. Use heap snapshots and the reported visible-tile scenario to determine whether decoded bands remain retained after band changes and pause; done means identifying a supported fix or confirmed recovery path with bounded memory behavior.

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.