強震モニタ描画の負荷と1年闘った話

この記事は [防災アプリ Advent Calendar 2022](https://adventar.org/calendars/7488) 17日目の記事です。
前日（16日目）は、Seto City Cam さんの「[**Pythonで地震情報を取得する**](https://qiita.com/Ri-chan041213/items/b8db38b485fea960e905)」でした。

# はじめに
昨年9月から **EarthQuickly** という地震ソフトを制作しています。

https://ameuma773.github.io/EarthQuickly/

そんな EarthQuickly の開発の歴史を振り返りつつ、軽量化について書いていきたいと思います。

    前提として... EarthQuicklyが利用しているフレームワーク・言語等
    C# WPF .NET6 ※開発当初は.NET5

:::note warn
万が一、間違った内容が書かれていた場合はこっそりご報告ください。
:::

# 本題
- EarthQuicklyの開発当初から、いずれ実装したい機能として以下の4つがありました。
    - **① 強震モニタの取得・アプリ側で描画して表示**
    - ② 緊急地震速報の取得・表示
    - ③ 地震情報の取得・表示
    - ④ 津波情報（津波警報等）の取得・表示

この中で、納得する実装ができるまで最も時間を要したのが **①** です。
.NETの場合、強震モニタからのデータ取得に関しては、[KyoshinMonitorLib](https://github.com/ingen084/KyoshinMonitorLib)というすごく使いやすいライブラリがあるので比較的に簡単に実装できます。
しかし、取得したデータを基に画面に観測点を描画していく際、問題になるものがあります。
それは **CPUへの負荷の大きさ** です。

# 負荷との闘い
EarthQuicklyは、長らく強震モニタ観測点を描画する際の負荷と戦い続けてきました。

地震ソフトというものは、基本的にバックグラウンドに常駐して地震監視を続けるものです。
そんな常駐ソフトが常にPCに高い負荷を与えるわけにもいかず、何としても負荷を減らしたいという思いがありました。
しかし、毎秒1000個以上の色の異なる円を少ない負荷で描画するのは至難の業だと思います。
そんな格闘の歴史を振り返るとともに、どのようにして軽量化を実現していったかを振り返りたいと思います。

> 技術的なことは主に 結論 で書くので、
さっさと軽量化の方法教えろ！って方は 結論 まで読み飛ばしてください。

## 2021年11月24日
[KyoshinMonitorLib](https://github.com/ingen084/KyoshinMonitorLib) の使い方が分かったことをきっかけに、強震モニタ観測点の描画にチャレンジしてみました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/8ecbb5a5-164e-a64f-3729-185c472c619a.png" width="700px">

日本列島の向きがおかしい等、ツッコミどころが多すぎます。
とにかくこの時のCPU負荷は驚異の **常時43%！**
PC全体の挙動に影響を及ぼすほど、絶望的に重かったです。


WPFには、図形描画オブジェクトである Shape が用意されていますが、当時はその存在を知らないので、
色を塗りつぶし正方形にしたTextBlockオブジェクト（文字を表示するためのコントロール）を毎秒生成して表示していくという、謎な実装をしていました。

さすがにこの負荷での実用化は不可能だったので、このバージョンはお蔵入りとなります。

## 2021年12月
[地震情報アプリ界隈 Advent Calendar 2021](https://adventar.org/calendars/6729)を見て、開発のモチベーションがかなり高かった時期です。
そんな中、2度目の強震モニタ観測点の描画にチャレンジします。

前回は

    強震モニタからデータ取得
    ↓
    観測点ごとにオブジェクトを生成して表示

という処理を毎秒行っていましたが
今回は

    起動時に観測点のオブジェクトを生成
    ↓
    強震モニタからデータ取得
    ↓
    前回取得時の色と比較
    ↓
    色が前回取得時と違えば、最初に生成した観測点のオブジェクトを参照して、観測点の色を変更する

という処理方法に変更しました。

前回取得した観測点の色と比較して、色が違う場合にのみ色変更の処理を行うことで、無駄なオブジェクトの参照を減らしています。
また、円描画用のEllipseオブジェクトを使うなど、他の地震ソフトと同じように観測点を四角形ではなく円で描画するように変更を加え、強震モニタ観測点の描画にリベンジすることにしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/56fb04aa-a6a6-de8b-bf8a-cbdd092ede55.png" width="600px">

気になるCPU負荷は... **通常時10%程度！**
まだまだ高負荷ではありますが、ギリギリ現実的な値となったので一旦実用化となりました。
この時点での通常時のCPU負荷の目標は **3%以下** でしたが、この目標が達成されるのは半年以上も先の事になります。

## 2022年1月〜3月
これ以上の軽量化の方法が思いつかぬまま、しばらくこの問題は放置されます。
しかし3月に入り、この負荷の問題がいよいよ牙を剥き始めました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/2f736a1c-7e82-bf15-455b-50767dcfe5dc.png" width="500px">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/b7bb18fb-5708-2573-aed2-9bd5909a2c00.png" width="600px">
3/16と3/18に立て続けに規模の大きな地震が発生しました。
規模の大きな地震は広範囲が揺れるため、多くの観測点の色を塗り替える必要があります。
地震が起こっていない状態でもある程度負荷がかかっているのに、一斉に多くの観測点を塗り替えたらどうなるでしょうか。
そうです。処理が追い付かず **ソフトがフリーズしてしまいます。**

主に開発に使用しているメインPCは、ある程度パワーがあるのでギリギリ耐えましたが、地震監視配信に使っているサブPCではパワーが足りずフリーズしてしまいました。

## 2022年4月（苦肉の策を発案）
この頃になると、前述の問題以外にも負荷の多さに起因する多くの問題が起こっており、より多くの新機能を追加していくためにも、早急な負荷の軽減が求められていました。
しかし、人間というものは追い詰められると急にひらめくものです。
とある**迷案**を思いつきました。それは...

「**観測点を表示しなきゃいいんだ...！**」
は？って感じかと思いますが、これがこの先2ヵ月以上負荷を「ごまかす」ことができる名案となりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/148b9492-d08e-67ac-978c-14ed31fa0560.png" width="700px">

↑ 2022/04/07 愛知県東部(M4.6)の地震
上が「通常モード」※これまでと同じく観測点を描画。
下が「簡易モード」※観測点の代わりに**細分区域**を塗りつぶす。

観測点を表示しない新たなモードを「**簡易モード**」と名付け、実用化しました。
※現バージョンにおいても、実測震度塗りつぶしモードとして活躍しています。

観測点を表示せずとも現在の揺れの広がりを掴めるよう、
細分区域内の強震モニタ最大震度の色で塗りつぶす処理になっています。

何より、細分区域ごとに色を塗り替えることで、これまで毎秒1000個以上の観測点のEllipseオブジェクトを参照する必要がありましたが、簡易モードではPolygonオブジェクト188個（＝細分区域の数）を参照して、適宜塗り替えるだけで済みます。

実際に通常モードと簡易モードで地震発生時の負荷を比較するとその差は本当に歴然で、
通常モード : 平均<strong>20%</strong>くらい
簡易モード : 平均<strong>10%</strong>くらい
と、通常モードに比べて簡易モードではおよそ半分の負荷で描画ができました。

※現在はさらに軽く細分区域の塗りつぶしができるようになっています。

## 2022年4月29日

このように試行錯誤を繰り返し、ある程度長時間起動しても安定して動くようになっていきました。

そしてこの日、EarthQuicklyはクローズドベータ版として **4名** のデバッガーの方の手に渡りました。
その後UIや機能面、不具合などについて多くの助言をいただきました。
この場を借りてお礼申し上げます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/7245f165-aeda-5129-f8fb-cf47a04fdc7e.png" width="700px">

## 2022年5月～6月
前述の「ごまかし」によってしばらく放置されていたこの問題ですが、決して問題が無くなったわけではありません。
「もう少し描画を軽くできないか」との声もいただきました。
そろそろ一般向けにリリースしてもいいかな...とも思っていたころですが、この描画の重さが解消できるまでは流石にリリースできません。今こそ描画を根本から変えていく時だと感じ、Google先生に助けを求めます。

Googleで見つけだしたMSのWPFのドキュメントのページをしらみつぶしに見ていると...
「[**DrawingVisual**](https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.drawingvisual?view=windowsdesktop-6.0)」「[**DrawingContext**](https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.drawingcontext?view=windowsdesktop-6.0)」というワードが目に入ってきました。

どうやら、デフォルトのWPFにおいてこの2つが最も軽量な描画方法と言えるようです。
しかも、円を描画する用の[**DrawEllipseメソッド**](https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.drawingcontext.drawellipse?view=windowsdesktop-6.0)まで用意されてるではありませんか！

早速、この方式で描画を試してみることにしました。

## 2022年6月25日
[公式ドキュメント](https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/graphics-multimedia/using-drawingvisual-objects?view=netframeworkdesktop-4.8) などを参考に、見様見真似で実装しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/59796890-f91a-ec17-dd73-be2f25b4f6f1.png" width="700px">

気になるCPU負荷は...通常時<strong>5%程度！</strong>
これまでよりも負荷がおよそ半分になりました。
これはいける...！と思い、急ピッチで一般向けリリースの準備を進めます。

どんな感じの処理にしたかというと...

    観測点の色を取得
    ↓
    前回取得時の色と比較する
    ↓
    色が前回取得時と違えば。Canvasを新たに観測点を上書き
    ↓
    最新の観測点の色を色の配列に格納

Drawしたオブジェクトは後から色などを変更したり参照したりすることはできません。（その代わりとても**軽い**）
そこで、色に変更があった観測点はCanvas上で **物理的に上書き** します。

:::note warn
注意点
このような処理を行う場合、必ず一定時間ごとなどにCanvasの中身（Children）をクリアーするようにしてください。
メモリーリークの原因になります。
:::

## 2022年7月
かくして2022年7月1日、EarthQuicklyのベータ版は一般向けにリリースされ、オープンベータ版となりました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Windows向け地震ソフト、「EarthQuickly」のベータ版を一般向けにリリースしました。<br>下記サイトからダウンロードできます。<br>（諸注意はリプ欄へ続く）<a href="https://twitter.com/hashtag/EarthQuickly?src=hash&amp;ref_src=twsrc%5Etfw">#EarthQuickly</a><a href="https://t.co/pcI9zDylvY">https://t.co/pcI9zDylvY</a> <a href="https://t.co/YeOzDD6AT0">pic.twitter.com/YeOzDD6AT0</a></p>&mdash; あめうま (@Ameuma773) <a href="https://twitter.com/Ameuma773/status/1542832633574731776?ref_src=twsrc%5Etfw">July 1, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

しかし一般公開後、多くの方から「**動作が重い**」という声をいただきました。

これでもかなり軽量化したほうなんですが、
やはり既存の地震ソフトと比べ、どうしても見劣りするPCへの負荷の多さが課題でした。

WPFではこれ以上の軽量化は無理なのかと、フレームワークの変更を検討した時期もありましたが、
これまで作ってきたコードを無駄にするわけにもいかないので、なかなか変更を決断できずにいました。

**しかし、**
ふと観測点描画のコードを見てみると、かなり非効率なコードを書いていたことに気づきました。

EarthQuicklyの処理には、
"**System.Windows.Media.Color**"と
"**System.Drawing.Color**"の二つの色を扱う構造体を併用しています。

基本的には、WPFで画面描画に使う色は基本的に "Media.Color" なのですが、
KyoshinMonitorLib から得られる観測点の色が "Drawing.Color" で得られるため、どうしてもこの二つが混在してしまいます。
（Skiaに統一することで混在を解消できるかもしれませんが...）

これらを相互変換する処理は**多くのリソースを消費する**ため、あまり多用することは推奨されません。

それを踏まえたうえで処理を見てみると、この当時の処理は以下のようになっていました。

    観測点の色を取得（Drawing.Color）
    ↓
    前回取得時の色と比較するため、取得した色を"Media.Color"に変換
    ↓
    前回取得時の色と比較する
    ↓
    色が前回取得時と違えば。新たに観測点を上書き
    ↓
    最新の観測点の色を"Media.Color"の配列に格納

この処理では、毎秒1000回以上（＝強震モニタ観測点の数）**色を変換する**ことになります。
なので、色を変換する回数を減らせば負荷が減らせるのでは？と考えた次第です。
そこで、下記のように処理を変えてみました。

    観測点の色を取得（Drawing.Color）
    ↓
    前回取得時の色と比較する
    ↓
    色が前回取得時と違えば、色を"Media.Color"に変換する。
    ↓
    変換した色を使って、観測点を上書き
    ↓
    最新の観測点の色を"Drawing.Color"の配列に格納

このような処理にすることで、色の変換を**最小限に**抑えることができます。
その結果は...
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">マジで軽くなった <a href="https://t.co/fHhVMgGr31">pic.twitter.com/fHhVMgGr31</a></p>&mdash; あめうま (@Ameuma773) <a href="https://twitter.com/Ameuma773/status/1548864415541637120?ref_src=twsrc%5Etfw">July 18, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

CPU負荷は、通常時およそ**3%程度**！

やっと目標としていた基準に到達することができました。
初めて描画を試してみてから7か月と24日が経った2022年7月18日の話です。

## 2022年8月～
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ここだけの話、最新版の負荷は一般リリース初版（v0.2.0）のおよそ6分の1にまで減らせてる <a href="https://t.co/ugMj35FMgF">pic.twitter.com/ugMj35FMgF</a></p>&mdash; あめうま (@Ameuma773) <a href="https://twitter.com/Ameuma773/status/1579859456711872514?ref_src=twsrc%5Etfw">October 11, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これまでの描画の工夫に加え、メモリ管理をより最適化した結果
CPU負荷は通常時でおよそ**1%台**になりました。

描画の負荷を減らすことで、ほかの機能へリソースが割けるようになりました。
これにより、地震の検出精度の向上や緊急地震速報の受信速度の向上など、
強震モニタ観測点の描画とは直接関係のない部分にも良い影響が及ぶようになり、
結果としてソフト全体のクオリティの向上につながりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/5f4d1e38-0b01-9951-9b56-31a09be55d56.jpeg" width="700px">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/0b431fc6-d0bc-8b0a-868e-82759bfb2a84.jpeg" width="700px">

# 結論
軽量化は優先して行うべきだと改めて実感しました。
ソフトが軽くなるだけでなく、**開発全体に余裕ができる**のが軽量化のメリットだと思います。

そして技術的な話ですが...
デフォルトのWPFで強震モニタ観測点の描画をしたい場合、以下の点を意識するとよいかと思います。

- 標準のShapeライブラリは**使わない**
- **DrawingContext + DrawingVisual** を使う
- **色の変換**は最小限にする

他の言語・フレームワークにも通じるものはあるかと思います。

# 最後に
この記事を書きながら、開発初期のころを思い出して懐かしい気分に浸っていました... **が、**
今まさに、これまで繰り広げてきた負荷との闘いが再び「**スマホ上**」で起こることになりそうです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2359589/2a5541ea-f046-4cae-7299-7620a8f59e82.png" width="400px">

**EarthQuickly For Mobile**
今週、開発がスタートしたスマホ版EarthQuickly、
来年はこれについての記事が書けたらいいなって思ってます...

[防災アプリ Advent Calendar 2022](https://adventar.org/calendars/7488)、
翌18日目は YumNumm さんの「[**Flutterで地震観測・速報アプリを開発しているお話**](https://qiita.com/YumNumm/items/ef71e6f5c98b6554028f)」です。

長い記事でしたが、最後までお読みいただきありがとうございました。