こんにちは、アカツキゲームスの有田です
Unity でゲームを開発していると、Editor 拡張の社内ツールがどんどん増えていきます
ビルドツール、アセット作成ツール、デバッグ用のボタンなど
作ったときは便利でも、しばらく経つとこんな疑問が湧いてきます
- このツール、本当に使われているんだろうか?
- 気づかないうちに重くなっていないだろうか?
- 消していいツールはどれだろうか?
私たちのプロジェクトでは、この問いに感覚ではなくデータで答えるために
社内ツールの利用計測の仕組みを作りました

何を作ったのか
社内ツールやデバッグコマンドについて
「どのツールを・いつ・何回・何 ms かけて」
使ったかを PostgreSQL に記録し、Grafana で可視化する仕組みです

ツール作者は何もしなくていい
計測の仕組みを作ってもツール作者全員に
「計測コードを入れてください」とお願いして回るのは現実的ではありません
そこで、ツール作者側の手間がゼロになることを最優先に設計しました
Unity Editor のツールは、[MenuItem] から起動されるため
ここを計測対象にすることで何もしなくても自動で回数が計測されます
[MenuItem("Tools/なんかビルドする")] // これだけで回数が計測される private static async UniTask BuildSomethingAsync()
処理時間も見たい場合は、attribute を 1 個足すだけです
[MenuItem("Tools/なんかビルドする")] [ToolTelemetryTimer] // 名前省略時は「型名.メソッド名」 private static async UniTask BuildSomethingAsync() // async でも完了まで計測される
attribute を付けられない場所には、Dispose 型の scope を使います
// ボタン押下からの一連の処理 using var scope = new ToolTelemetryTimerScope("MyTool/Export"); // ウィンドウの表示時間を測る(フィールド保持パターン) private ToolTelemetryTimerScope _scope; private void OnEnable() => _scope = new ToolTelemetryTimerScope("MyWindow"); private void OnDisable() => _scope.Dispose(); // その場で 1 カウント new ToolTelemetryCountScope("MyTool/SomeButton").Dispose();
仕組み:Harmony で計測コードを差し込む
「何もしなくても計測される」を実現するには
既存ツールのコードに手を入れず、外から計測処理を差し込む必要があります
そこで、実行時にメソッドを書き換えられるライブラリ Harmony を採用しました
Mod 開発などで広く使われているライブラリです
Editor 起動時の初期化で
Unity API の TypeCache.GetMethodsWithAttribute<T>() を使って高速に下記の attribute が付いてるメソッドを列挙します
| attribute | 内容 |
|---|---|
| ToolTelemetryTimer | 回数 + 処理時間 |
| ToolTelemetryCount | 回数のみ |
| MenuItem | 既存の全ツール、回数のみ |
これらのメソッドに harmony.Patch(method, prefix, postfix) で計測コードを注入します
計測に必要なのは、次の 2 つです
- Prefix:
Stopwatch.GetTimestamp()を__stateに保存する(処理時間を測る対象のみ) - Postfix: 計測結果(経過時間や回数)をキューに積む
// TypeCache で列挙したメソッド foreach (var method in targetMethods) { var prefix = new HarmonyMethod(typeof(ToolTelemetryPatcher), nameof(Prefix)); // Postfix は戻り値の型で出し分け(次節) harmony.Patch(method, prefix: prefix, postfix: SelectPostfix(method)); } // 計測開始時刻を __state に保存する共通 Prefix private static void Prefix(out long __state) { __state = Stopwatch.GetTimestamp(); }
計測側で例外が起きても警告ログを出すだけに留め
Harmony の初期化に失敗した場合も計測を無効化するだけにしてあります
計測の都合でツール本体や Editor を巻き込まないためです
async メソッドの計測
async メソッドは最初の await で呼び出し元へ戻るため、Postfix が
走るのは処理の完了時ではなく、Task / UniTask が返った瞬間です
戻り値の型ごとに Postfix を出し分けて、完了までを計測するようにしました
Task / Task<T>
参照型なので
返ってきた __result に ContinueWith を繋ぐだけで完了時刻が取れます
// Task を返すメソッド用 Postfix // 返ってきた Task に継続を繋ぎ、完了(例外・キャンセル含む)時点で記録する private static void PostfixTask(MethodBase __originalMethod, long __state, Task __result) { __result.ContinueWith(_ => Record(__originalMethod, ElapsedMs(__state))); }
UniTask / UniTask<T>
struct で原則 1 回しか await できず、継続を「繋ぐ」ことができないため
代わりに ref __result で戻り値ごと計測用ラッパーに差し替えます
// Harmony の Postfix は ref __result で「呼び出し元へ返る戻り値」を差し替えられる private static void PostfixUniTask(MethodBase __originalMethod, long __state, ref UniTask __result) { __result = Wrap(__result, __originalMethod, __state); } // 元の UniTask を包み、完了(例外・キャンセル含む)まで待ってから記録する private static async UniTask Wrap(UniTask inner, MethodBase method, long start) { try { await inner; } finally { Record(method, ElapsedMs(start)); } }
async void / UniTaskVoid
完了を外から観測できないので、回数のみ記録します
// 完了を追跡できないメソッド用 Postfix、回数のみ記録する private static void PostfixCountOnly(MethodBase __originalMethod) { RecordCount(__originalMethod); }
どんな情報を送っているのか
データベースのテーブルはツールの利用状況を記録する tool_usage の 1 本だけです
1 実行 = 1 行のイベントログ型で、集計は Grafana 側に任せています
| カラム | 内容 |
|---|---|
| tool_name | ツール名(MenuItem パス / attribute 指定名 / 型名.メソッド名) |
| duration_ms | 処理時間(NULL = 回数のみ記録) |
| executed_at | 実行時刻 |
送信経路は 3 段のバッファ構成で、Editor を止めないことを最優先にしています
計測 → ConcurrentQueue(メモリ) → 60 秒ごとに JSON Lines でスプールファイルへ退避 → PostgreSQL へ バッチINSERT、成功分だけスプールから削除
- メインスレッドでやるのは 60 秒間隔の時刻チェックだけで、ファイル I/O はスレッドプール、DB アクセスは専用ワーカーが行います
- VPN 未接続などで DB に届かない場合は何もせずスプールに残し、次回接続時に再送します
- Editor 終了時はキューをスプールへ退避するだけで、終了をブロックしません
実機のツールも計測する
デバッグメニューなど、実機(開発ビルド)でしか使わないツールも多くあります
Editor 限定の計測ではここが見えないので、実機の記録も同じ tool_usage テーブルへ登録しました
社内には、実機と開発 PC を WebSocket で常時接続するデバッグ基盤がすでにあります
これに相乗りする形で、実機の記録も PC 経由で DB へ送るようにしました

実機側でやっているのは、既存接続へイベントハンドラを 1 個登録することだけです
接続管理や再接続のコードは 1 行も書いていません
計測結果から見えたもの
実際にデータを眺めることで改善した事例を 2 つ紹介します
サウンドデザイナーは
チェックのために BGM / SE / Voice のオンオフを頻繁に切り替えます
GameView 上でデバッグウィンドウを開く → Sound タブへ移動 → BGM をオフにする
という操作を、繰り返し行っていました

1 回あたりは数クリックでも、毎日繰り返せば手間になります
本人たちは不便とも思っておらず、要望として上がってくることはありませんでした
こうした小さな手間は、データを見て初めて気づけます
そこで、どこからでもワンクリックで BGM / SE / Voice を切り替えられるよう
Unity Editor のトップバーにトグルボタンを置きました

もう一つは、時刻設定のデバッグ機能です
こちらもデータを確認すると多数の方が利用していたので
UI を見ると、なんと Select ボタンを連打して時刻を設定していました
これは非常に面倒な操作です
しかし最初期からある機能のためか、使いづらいという声は聞いたことがありませんでした

ダイアル形式の UI に作り直すことで、改善できました

まとめ
社内ツールの利用計測の仕組みと、データから改善につながった実例を紹介しました
ツールが使われているかどうかを、作者の善意や利用者の申告に頼って把握するには
限界があります。自動で計測される状態にしておけば、作りっぱなしのツールも勝手にデータを残してくれます
みなさんのプロジェクトにも、使われなくなったツールが眠っているかもしれません
まずは計測から始めてみてはいかがでしょうか?
Tips:導入後トラブル
Unity が起動しなくなった
この仕組みを導入後、Unity を起動し直すと起動できなくなる問題が発生しました
Unity 起動時に
Failed to find entry-points ... BadImageFormatException: Read out of bounds が出て、プロジェクトが開けなくなります
原因は、NuGet で配布されている Lib.Harmony の DLL 側にありました
私たちが採用した v2.4.1 を含む 2.3 以降の配布 DLL には
「存在しない AssemblyRef を参照したままの TypeRef」がメタデータに残っています
実際に NuGet から取得した DLL を走査して確認しました
通常のランタイム実行ではこの不正な TypeRef が使われることはないため無害ですが
Unity のプロジェクトオープン時にメタデータを全部読むため、不正な TypeRef を読んだ時点で BadImageFormatException が発生します
Mono.Cecil で DLL を修復する
Mono.Cecil で DLL を読み込んで書き出し直すとメタデータ内の不正な TypeRef が消えることが分かりました
そこで、次の処理を行うスクリプトを作りました
- NuGet 公式から指定バージョンの nupkg を取得する(改変前後を追跡できるよう SHA-256 を記録)
- Cecil の read→write でメタデータを再生成する
- 機械検査: 不正 TypeRef が 0 件、かつ TypeDef / MethodDef 数が修復前と一致しなければ、配置せず異常終了する
- 修復済み DLL をプロジェクトの Editor 専用 Plugins フォルダへ配置する
修復版に差し替えた後は、起動し直しても問題は起きず
実行時パッチも正常に動いています














