Skip to content

Apply marker changes to the grid index instead of rebuilding it - #20

Merged
wf9a5m75 merged 1 commit into
developfrom
perf/grid-index-incremental
Nov 5, 2026
Merged

wf9a5m75 merged 1 commit into
developfrom
perf/grid-index-incremental

Conversation

@wf9a5m75

@wf9a5m75 wf9a5m75 commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

#19(K-D 木の削除)の上に積んでいる。#19 をマージしたあと、base を develop に切り替える。

不具合

MarkerGridIndex は、マーカーの追加・削除・更新のたびに捨てられ、次の検索が全件の作り直しを待っていた。パンのたびにマーカーを差し替えるアプリでは、次のタップとすべてのタイル描画がこれを待つ。ドラッグでは毎フレーム待つ。

Pixel 5a で 300 件を差し替えた直後の検索:6 万件で 83 ms、14.4 万件で 212 ms(最大 780 ms)。js-sdk-core#20 で web が直したのと同じ種類の停止で、Android の方が重かった。

修正

  • 追加:次のスロットを渡し、エントリは未反映の一覧に置く。次の検索で、その分だけを並べ、整列済みの配列と 1 回の走査で合わせる
  • 削除:スロットを空にするだけ。検索は空きを飛ばし、次の合わせ込みで空きのエントリを落とす
  • 同じセルのままの差し替え(アイコンやフラグの変更):スロットの参照を入れ替えるだけ
  • 作り直すのは、スロットの半分が空いたとき(4,096 以上)と、消すように言われたマーカーが見つからないとき(競合などで索引が実態とずれた場合に、信用せず作り直す)だけ
  • マーカーごとの対応表は持たない(14.4 万件で数 MB かかるので、最初の版で避けた経緯がある)。消すエントリは、今の位置のセルを二分探索し、同一オブジェクトで照合して見つける。ドラッグのように位置をその場で書き換えてから知らされたものは別のセルにあるので、その時だけスロットを端から探す
  • 間引き検索:セルの最後のエントリを代表にしていた。空きを飛ばして最後の生きているものを選ぶので、代表が消えたセルも抜けない。新しく足したものはスロット番号が大きく、セルの後ろに並ぶので、「最後に足したものが代表」は変わらない

MarkerManager は invalidate() の代わりに add / remove / replace を呼ぶ。clear() は従来どおり invalidate()。公開 API の変更は無い(MarkerGridIndex は internal)。

確認

  • ./gradlew testDebugUnitTest:290 件すべて通る

  • MarkerGridIndexTest に追加

    • 作った索引に、追加・削除・同じ位置での差し替え・別の位置での差し替え・その場での移動(ドラッグ)を 1,000 回混ぜ、200 回ごとに範囲検索・最近傍・間引き検索が総当たりと一致する(範囲検索はオブジェクトの同一性まで見る)。その間、マーカーの集合を一度も読み直さない(作り直していない)
    • 代表が消えたセルを、間引き検索が一つ前のもので埋める
    • 見つからないマーカーを消すと作り直す。半分以上消すと作り直して詰める
    • 実装をわざと壊して(同じセルの差し替えで参照を替えない/空きを飛ばさない/その場で動いたものを探さない/削除で空けない)、それぞれ落ちることを確かめた
  • ktlint:main・test とも指摘なし

  • Pixel 5a(日本全域にばらまいたマーカー、一時的な計測用テスト。コミットしていない)

    6 万件 前 → 後 14.4 万件 前 → 後
    300 件差し替え直後のクリック検索 83 → 1.2 ms 212 → 3.0 ms
    同・タイルの間引き検索 84 → 5.5 ms 212 → 3.5 ms
    アイコン更新 300 件+検索 80 → 9 ms 195 → 8.5 ms
    ドラッグ 1 フレーム(移動+検索) 73 → 0.05 ms 189 → 0.05 ms
    最初の検索(索引の作成) 249 ms 317 ms

    最初の検索は 1 回だけで、実行ごとのばらつきの範囲(同じ条件の前回計測で 191〜394 ms)。

ios-sdk-core にも同じ変更を入れる。

MarkerGridIndex was thrown away on every add, remove or update, so the first
query after any change paid the full build. An app that swaps markers on
every pan paid it on its next tap and on every tile; on a Pixel 5a, after
300 swaps, the next query took 83 ms at 60,000 markers and 212 ms at 144,000,
and every frame of a drag paid it too.

A marker that arrives now takes the next slot, and its entry waits in a
pending list; the next query sorts only those and merges them into the
sorted array in one pass. A marker that leaves empties its slot, which
queries skip and the next merge drops. A replacement that stays in its cell
swaps the reference in its slot and nothing more. The index rebuilds only
when half its slots are empty or when it cannot find a marker it was told
to forget.

Nothing is kept per marker to find its entry: it is in the run of the cell
its position falls in, matched by identity. A marker moved in place before
it is reported, as a drag does, has its entry in another cell's run; for
that one the slots are scanned.

The thinned query took a cell's last entry; with emptied slots it steps back
to the last live one, so a cell whose representative left keeps one.

Same 300 swaps on the Pixel 5a: 1.2 ms at 60,000, 3.0 ms at 144,000. A drag
frame goes from 189 ms to 0.05 ms at 144,000.
Base automatically changed from chore/remove-kdtree to develop November 5, 2026 09:14
@wf9a5m75
wf9a5m75 merged commit b462152 into develop Nov 5, 2026
2 checks passed
@wf9a5m75
wf9a5m75 deleted the perf/grid-index-incremental branch November 5, 2026 09:14
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