Raster-array timeline playback: visible-tile decoded band ArrayBuffers accumulate in JS heap; memory does not drop after pause; partial tile eviction ineffective
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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