Skip to content

Let a state drop a destroyed controller; pool tile scratch buffers - #26

Merged
wf9a5m75 merged 2 commits into
developfrom
fix/mapview-teardown
Nov 8, 2026
Merged

wf9a5m75 merged 2 commits into
developfrom
fix/mapview-teardown

Conversation

@wf9a5m75

@wf9a5m75 wf9a5m75 commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

なぜ

プロバイダを切り替えるたびに前の地図がメモリに残り、区画データ(約 10 万ポリゴン)を出したまま数回切り替えると OutOfMemoryError で落ちていました。原因はいくつかのリポジトリにまたがっていて、core が持っているのは次の 2 つです。

  • state が破棄済みのコントローラを持ち続ける: アプリは state を地図より長く持ちます(example-app はプロバイダごとに 1 つ持っています)。state は最後に接続したコントローラを放さないので、地図を破棄したあとも、そのコントローラ経由で古い地図のオーバーレイがすべて残っていました。
  • スレッドごとの作業用バッファ: TilePngEncoder と MarkerTileRenderer は、作業用のバッファ(このタブレットのタイルサイズで約 1.2 MB)を ThreadLocal に入れていました。タイルは地図 SDK のスレッドで描かれます。Google Maps は地図を作るたびに androidmapsapi-tileoverlay のスレッドを 3 本作り、地図を破棄しても止めません。そのため Google の地図を 1 回作るごとに、生きたスレッド 3 本とバッファ 3 つが残り、切り替え 1 回あたり約 1.7 MB ずつ増えていました。

何を変えたか

  • MapViewState.detachController(controller) を追加しました(公開 API の追加)。渡された controller がまだ接続中のときだけ外します。呼ぶのは compose の MapViewBase です。
  • Polygon / Polyline / Circle / GroundImage / RasterLayer の各コントローラの destroy() で、manager を clear() するようにしました。破棄済みのコントローラがどこかから参照されていても、エンティティは残りません。破棄と並走した add() は、!! で落ちずに早期 return します。
  • ScratchPool(internal)を追加し、TilePngEncoder と MarkerTileRenderer の ThreadLocal の作業用バッファを置き換えました。スレッドがいくつ増えても、プールが持つのは最大でコア数程度(2〜8 個)です。スレッド自体は SDK のもので、こちらからは止められません。
    • internal ですがバイトコード上は public になるので、.api に載っています。

テスト: detachController の 2 本と ScratchPool の 3 本を足しました。

関連 PR(同じ修正の一式)

fix/mapview-teardown は次のリポジトリにまたがります。core → compose → 各プロバイダの順にマージしてください。プロバイダの変更は、core の MapViewState.detachController と、compose の MapViewBase(onDestroyMapView = …) / MapViewLifecycleOwner を使います。

検証

タブレット(HA26Z6JL)の example-app で確認しました。区画データ(約 10 万ポリゴン)はポリゴンのタイル描画が要るので、検証には、このブランチのコミットを feat/polygon-tiling / feat/overlay-on-added の上に一時的に cherry-pick したブランチ(test/teardown-on-tiling、push していません)を使っています。

  • 地図の切り替え(Boston の City parcels、MapLibre ⇄ Google): 毎回 am dumpheap -g で GC させて、GC 後のヒープを測りました。
    • 修正前: 292 → 489 MB と増え続け、5 回目の切り替えで OutOfMemoryError(512 MB)で落ちました。ヒープダンプでは、切り替え後も古い MapLibreViewController / GoogleMapViewController が残り、それぞれ 98,843 件の PolygonEntity を抱えていました。
    • 修正後: 24 回切り替えても、GC 後のヒープは 307〜309 MB で一定でした。Native alloc も増えていません。
  • 全プロバイダの巡回(11 種類、City parcels): 巡回後のヒープダンプに残った地図コントローラは、表示中の 1 個だけでした。PolygonEntity も表示中の地図の 98,843 件だけです。FATAL は 0 件でした。
  • 回転(横 → 縦 → 横): map-design ページでデフォルト以外のデザインを選んでから回しました。Google / HERE / Mapbox / ArcGIS 3D / ArcGIS 2D / MapLibre / TomTom / MapTiler / Longdo で、デザインとカメラが保たれ、プロセスも落ちませんでした。
  • 単体テスト・apiCheck: develop を起点に 12 モジュールで testDebugUnitTest と apiCheck を流しました。新しく足したテストはすべて通っています。失敗は次の 5 つで、どれも develop の時点で同じ理由で落ちています。
    • apiCheck: arcgis / maplibre / mappls / openmobilemaps
    • openmobilemaps の conformance テスト 1 件

確認できていないもの:

  • Mappls の回転: 検証用のビルドでは Mappls の認証設定が無く、初期化に失敗していました。
  • OpenMobileMaps の回転時のデザイン保持: example のデザイン選択 UI が OMM の state に対応していません(カメラの保持は確認済み)。

…stroy

The state outlives the map: apps keep it (the example app keeps one per
provider), and it held the last controller after the map was torn down,
and through it every overlay entity of that map. detachController(controller)
lets go of it, unless another controller has been attached since.

Overlay controllers' destroy() now clears their managers, so a destroyed
controller that is still referenced keeps no entities. An add() in flight
when that happens stops instead of tripping over the missing entity.
TilePngEncoder and MarkerTileRenderer kept their scratch (a ~1.2 MB direct
buffer at this tablet's tile size, an output stream) in a ThreadLocal. Tiles
are drawn on the map SDK's threads, and Google Maps starts a pool of three
"androidmapsapi-tileoverlay" threads for every map and never shuts it down,
so each destroyed Google map left three live threads with a buffer each:
about 1.7 MB of Java heap per provider switch that nothing could free
(heap dumps after GC, MapLibre <-> Google with Boston parcels).

ScratchPool keeps at most a few of them (about one per core) whatever the
threads do. The threads themselves are the SDK's and remain.
@wf9a5m75
wf9a5m75 merged commit d890677 into develop Nov 8, 2026
2 checks passed
@wf9a5m75
wf9a5m75 deleted the fix/mapview-teardown branch November 8, 2026 03:17
wf9a5m75 added a commit that referenced this pull request Nov 8, 2026
Provider CI builds this branch of core with compose's default branch,
which already uses MapViewState.detachController(controller) from
develop (#26).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant