AIは「間引きましょう」と言った。私は「全部出す」と断った。
第3回の今回は、約14万本の街路樹データで発生した描画性能の問題を題材に、AIが提案する「間引き」を断りSnapshotStateの構造を見直すに至った判断の経緯について解説します。
6:30
はじめに
こんにちは。私は、複数の地図SDKを宣言的UIから同じように扱えるライブラリを、OSSとしてAIを使って開発しています。同じ地図の上に情報を表示するだけなのにGoogle MapsやMapbox、ArcGISなどの地図SDKごとに操作するためのAPIが違います。だから全て同じように扱えるようにしたら便利になるライブラリです。
ただし、本連載の内容は地図SDKの話ではありません。「このような抽象化ライブラリの開発をどう行うか」「AIとの向き合い方」の話です。
前回、日本全国の郵便局、約24,000箇所を表示する話をしました。今回はその続きとして、約14万本の街路樹データを地図に載せたときに踏んだ地雷と、AIと一緒に原因を探して解決するまでの話をします。
ただ、本当に書きたいのはチューニングの手順ではありません。この半年でコードを書いているのが私ではなくAIになりました。 それでも開発が成立しているのはなぜか。人間の側に何が残ったのか。それを、実際に起きたこととして書いてみたいと思います。
宣言的UIは、何を引き受けてくれたのか
iOSでは「SwiftUI」、Androidでは「Jetpack Compose」、Webでは「React」。これらのフレームワークは「宣言的UI」というデザインパターンを採用しています。これは「画面に表示する状態をコードで書く(宣言する)」という方法です。
例えば、現在開発しているライブラリでは、以下のように書きます。MapView の下に Markers があることで、あなたは地図に詳しくなくても「地図の表示領域 (MapView) があり、その中に複数のマーカー (Markers) があるんだな」というのは理解できるでしょう。
MapView(state = mapViewState) {
Markers(states = markers)
}マーカーを管理するMarkerState を変更すると、その下にある地図上に表示されているマーカーを変更します。開発者は実際の地図SDK固有のマーカー操作APIを知る必要はありません。
// positionを変更すれば、「自動的に」地図上のマーカーを新しい位置に再描画
markers[i].position = GeoPoint.fromLongLat( <新しい緯度経度> )これは本当に楽になりました。画面の状態を自分で細かく追いかけなくてよくなった、という意味で、宣言的UIは開発の生産性を大きく変えたと思います。
ただ、楽になったことの裏側で、1つ手放したものがあります。いつ、何回、画面を作り直すかを、自分で決めなくなったことです。
決めているのはフレームワークと、その上に私が書いた「変化を見つけるしくみ」です。今回の話は、まさにそこで起きました。
最初は教科書どおり
開発初期の目的は「まず動くこと」です。大きな数は一旦忘れて、マーカー100個くらいの規模で考えます。
やりたいことは単純で「プロパティが変化したら、地図SDKに伝える」。AndroidならmutableStateOf で実現できます。値が変化したらイベントが発生し、それを後続のFlowなどで受け取れる、いわゆる「イベント」と「イベントリスナー」の関係です。
// 各プロパティが変化するとイベントが発生する、くらいの大まかな理解で良いです
class MarkerState(
position: GeoPoint, // 初期値
...
) {
var position by mutableStateOf(position) // 実際のプロパティ
var clickable by mutableStateOf(clickable) // 他のプロパティも同様
...
}あとは、これを全マーカー分、監視します。
// 全てのMarkerStateを1本ずつ監視する
val markers = scope.markerFlow.collectAsState()
markers.value.forEach { markerState ->
LaunchedEffect(markerState.id) {
// どれかのプロパティが変化したら controller.updateMarker が呼ばれ、
// 地図上のマーカーが更新される
markerState.asFlow().debounce(100).collectLatest {
controller.updateMarker(markerState)
}
}
}Jetpack Compose でUIコンポーネントを作る方法としては、ごく一般的なやり方です。このときの私は、AIにレビューをさせる側でした。設計を決めて、コードを書いて、「おかしいところある?」と聞く。AIは誤字や抜けを拾ってくる。その程度の関係です。
そして、この実装はマーカーが100個程度なら何の問題もありません。問題は数です。
約14万件という厚い壁
地図アプリで難しいのは、扱うデータ数が多いことです。前回の郵便局24,000件でも十分に大きいのですが、より実用的なデータとして、東京都の街路樹 144,183本を地図に描画することにチャレンジしました。
実行すると、明らかに反応が鈍い。表示はできるものの、地図を指でドラッグすると、数秒遅れて地図がついてきます。
AIに「遅いから何とかして!」と丸投げしても、うまくは解決できません。実際に返ってきたのがこれです。
「14万件のデータが多すぎるんです。データを間引きましょう」
ライブラリですから、与えられたデータを勝手に間引いたら駄目ですよね。「全部表示できます」と言っておいて裏で捨てていたら、それはもうライブラリとして嘘をついています。AIが見当違いなことを言ってくると「そうじゃないだろっ!」と無言で画面の前で空振りしています。
ただ、AIが悪いわけでもないんです。AIはその場のコードを読んで、一番効きそうな手を答えます。「このライブラリは与えられた点を1つも捨ててはいけない」というのは、コードのどこにも書いていない私の約束事でした。知らないことは答えられません。
ここが、この記事で最初に出てくる「人間が決めたこと」です。何を守るか。これはAIに決めさせてはいけません。
まず、測らせる
AIを使ってデバッグする場合も、手順は人間がやってきたことと同じです。AIを使わずにデバッグしてきた時代の経験値が、そのまま効きます。
まず探るべきは、どの動作が遅いのかです。
- 地図が初期化されてからマーカーが描画されるまではどうか
- 地図を指でドラッグしたときに、どの部分のコードが実行されるのか
- そのとき変化する変数やプロパティは何か
実機を接続して、adbコマンドやiOSの自動テストスクリプトを使わせて、1つずつAIに直接測らせます。勘所があるなら、それも伝えます。
これは、仕事で部下や後輩と一緒にデバッグする作業に似ています。いきなり直させるのではなく、まず問題を認識させるところから始める。自分の中では既に答えが見えていることでも、敢えて同じステップを踏ませて確認させます。
この地道な計測が、結果的に一番効きました。数字が出てくると、AIの答えが「それっぽい一般論」から「このコードの話」に変わります。
そして、出てきた数字は予想と違っていました。
描画は犯人ではなかったのです。マーカーを4分の1に減らすと、重さもちょうど4分の1になりました。描いているものの問題なら、画面に見えている数で決まるはずです。見えていない分まできっちり効いているということは、描画の話ではありまえせん。
原因は「変わっていないことの確認」
犯人は、プロパティの変化を見つける仕組みのほうでした。
地図をドラッグしているとき、マーカーは1本も変わっていません。変わっているのはカメラの位置だけです。木が勝手に引っ越したりはしません。
ところが、ライブラリは毎フレーム、14万本すべてに問い合わせていました。「あなた、変わっていませんか?」と。1本につき6つのプロパティを見るので、毎フレーム約86万回。そして、答えは86万回とも「変わっていません」です。
何も変わっていないことを確認するためだけに、毎フレーム86万回。。。(汗)
学校の出欠確認に似ています。先生が毎時間140人全員の席を回って「変更ありませんか?」と聞いて回るのか、変更がある生徒だけが手を挙げるのか。前者は人数に比例して重くなります。たとえ、その日誰も休んでいなくても。
直し方は、こう書くと当たり前に見えます。
聞きに行くのをやめて、変わった側に言わせる。
マーカーが「変わりました」と手を挙げるようにして、ライブラリは手が挙がった分だけ仕事をする。ドラッグ中は誰も手を挙げないので、仕事はゼロになります。「Pixel 5a」という、本記事執筆時点でも十分に古いAndroid端末上での計測で、大幅な改善に成功しました。
| 変更前 | 変更後 | |
| カクついたフレーム | 17.7% | 1.4% |
| 遅い側10%のフレーム時間 | 350ms | 21ms |
| 遅い側1%のフレーム時間 | 700ms | 24ms |
マーカーを1本も置いていない空の地図が 0.2% / 16ms / 19ms です。14万本を表示している地図が、何も表示していない地図とほぼ同じ数字です。
ちなみに細かい技術的な話は省略していますが、話としては単に全件走査をやめたのではなく、ComposeのSnapshot変更検知そのものが大量のStateに対してスケールしない問題をAIを使った実測から特定したということです。
一度捨てた設計に、戻りかけた
ここで1つ、白状しておくことがあります。
最初に見せた「全マーカーを1本ずつ監視する」コード。あれは14万本だと監視が14万個できてしまうので、途中で捨てていました。監視を1本にまとめて、そこから全員に聞きに行く方式に変えたのです。 そして、その1本が、今度は14万件を抱えて重くなった。
つまり、今回の問題は「過去に一度解いたはずの問題が形を変えて再発したもの」でした。
修正案を検討しているとき、「1本にまとめるのをやめて、またマーカーごとに監視すればいいのでは」という方向の話が出ました。それは、前に捨てたほうの設計です。
引っかかったのは私でした。「それ、前にやって駄目だったやつでは?」と。ただ、正直なところ記憶は曖昧でした。なので裏取りはAIにやらせました。過去のコミットを全部読ませて、いつ、何を、なぜ捨てたのかを拾わせる。出てきました。しかも当時の自分が、理由まで含めて丁寧に書き残していました。
ここが面白いところで、「前にそれで失敗した」という引っかかりは人間側からしか出てこないのに、それを証明する作業はAIのほうが圧倒的に速いんです。片方だけでは辿り着けませんでした。
結果として、どちらの設計も採りませんでした。1本ずつ監視しても、1本にまとめて全員に聞いても、毎フレーム14万件分の仕事をすることに変わりはない。聞く側の都合を変えても駄目で、聞くのをやめるしかなかったのです。
いつの間にか、書く人が入れ替わっていた
ここまでの作業で、私が書いたコードはほとんどありません。
最初の実装は、私が設計してAIにレビューさせました。今は逆です。AIが設計してコードを書き、私がやったのは議論とテストでした。
理由は明確です。Jetpack Compose が内部で変化をどう見つけているか、というレベルの話は、正直なところ私の持ち分を超えています。開発を始めた頃と比べて、AIはこの手の話を桁違いによく知るようになりました。私が何年かけて貯めた知識は、とっくに追い越されています。
これは、悔しいというより、素直に助かっています。自分が分かっている範囲のことしかできない開発より、分かっていないことに手が届く開発のほうが、作れるものが大きいからです。
ただし、1つだけ強調しておきたいことがあります。
書く人が替わっただけで、決める人は替わっていません
ここまでの作業で、私が書いたコードはほとんどありません。代わりに行ったのは「議論」です。
テストが人間の仕事になった
もう1つ、人間の側に残った仕事があります。「テスト」です。
新しい方式には弱点があります。「変わりました」と手を挙げるのを、どこか1つのプロパティが忘れていても何も落ちないし、何のログも出ません。 ただ、そのプロパティだけ地図に反映されなくなります。静かに壊れるのです。
AIが書いたコードを人間が1行ずつ読み切れない時代に、この手の静かな故障をどう防ぐか。私がやったのは、守ってほしいことを文章としてテストに書くことでした。
- プロパティを書き換えたら、地図に届く
- 同じ値を書き直したときは、届かない
- 短い間に連打したら、最後の1回に畳まれる
- 消したマーカーは、もう何も言ってこない
- 2つ同時に変わったら、2つとも届く
これは実装の話ではなく、約束の話です。だから中身をどう作り替えても、この5行は変わりません。実際、このテストは一度、追加したつもりで動いていなかったことが後から発覚しました(ビルド設定の配線が漏れていた)。慌てて直しました。テストは書いた時点では成果ではなく、動いていることを確認して初めて成果です。 これもAIに任せきりにできないところです。
コードの行はAIが持つ。仕様の文は人間が持つ。 今のところ、この分担が一番うまくいっています。
次回予告
AIからの「間引きましょう」という提案を断ったのは、正しかったと思います。14万本は1本も間引かずに、空の地図とほぼ同じ滑らかさで動いています。
この半年で変わったのは、誰がコードを書くかでした。変わらなかったのは、
- 何を守るかを決めること
- 答えに納得せず問い返すこと
- 正しさを文章にしておくこと
この3つです。たぶん、しばらくは人間の側に残ります。
AIに[YES]を押すだけの人になると、この3つは誰も持たなくなります。そうなったとき、出来上がるのは「動いているように見えるもの」です。14万件のデータを、黙って1万件に間引いて表示するライブラリのように。
次回は、このライブラリをiOSとReact にも同じ構造で展開している話を書く予定です。同じ設計を3つのプラットフォームで揃える作業こそ、AIが一番強い領域でした。
この記事をシェアしてください
