この記事について
Unity のProfierからどうにかしてデータを取って来れないかと筆者が悪戦苦闘の末作成したProfilerReaderについて書いたものです
https://github.com/unity3d-jp/ProfilerReader
Unity Profilerから書き出すログデータを取ってきて、集計データをcsvに書き出すツールです。
CSVの形式はいくつか用意したものがありますが、ユーザー自身でもIAnalyzeFileWriter等のインターフェースを継承したクラスをつくれば独自にCSV書き出し出来るようにするなども考慮しています。
なぜProfilerからデータを取ろうとしたのか…?
Unityでパフォーマンスチューニングをする際にUnityProfilerは非常に役立つツールです。
仕事の上でもよく利用していました。
ただ不満点がありました…。
それは、1フレーム毎にデータを閲覧する手段のみで、「平均化されたデータの取得や特別に何かが起きたフレームを見つける」と言ったことが困難であった事です。
UI表示で色々見えるのも良いのですが…、とにかくデータを抜いて自分が求める形で出力されて欲しい…
そう思ったのが、データを抜き出すきっかけでした。
データ抜き出しにあたり、最初に二つの手法が検討されました
思いついた当初、下記の二つの手法が検討されました。
1.UnityのAPI等を経由して、ProfilerWindowに表示されている内容を抜き出すという方法
2.保存されたログファイルを直接読む方法
1の手法の方が、まっとうな形でスジが良いので、こちらの手段を最初は検討しました…。
が、この手法ではProfilerWindowに表示される300フレームしかデータを取れないという問題がありました.
300フレームだと、「1分前後のゲームプレイをProfilerで岐路気宇しておいて…。後で何となく平均にして見たい!ボトルネック部分だけ見たい」みたいな事が出来ないなぁと思い、1の案は没にしました。
Profiler.logFileを指定してランタイム上で書き出されたログについては、300フレームを超えても情報が保持していたこともわかっていましたので…
「2.保存されたログファイルを直接読むようにしてしまおう」と言う事にしました。
( これが地獄の始まりでした…)
余談ですが…
1の手法を用いて作成されたのが PackageManagerからダウンロードできるProfilerAnalyzerです。
https://blogs.unity3d.com/jp/2019/05/13/introducing-the-profile-analyzer/
UnityEditorInternal.ProfilerFrameDataIterator というAPIを経由してデータを取ってくるようになっています。
どうやってバイナリデータを解析したのですか…?
Unity社員特権で…エンジンのコードをみながら移植しました。
C++側に実装されていたシリアライズ周りのコードを見ながら、C#に手で移植しています。
地道に移植&デバッグです。
バイナリデータ二つの形式
UnityのProfilerの書き出すバイナリファイルは、大きく二つの形式があります。
ランタイム上で書き出す.raw形式。ProfilerWindowの保存で行われる.data形式です。
.raw形式について
ランタイム上で書き出す時に、少しでも実行時のパフォーマンスに影響が与えないように、データ量も少なくなるようにとされた形式です。
どの関数(Sample)に何m掛かったかなどの集計はせずに、イベントが起きたタイミングの身を記録するようにしています。
例えば、「Sampleが何ms時点で始まりました」「Sampleが何ms時点で終わりました」という形のデータ形式になっています。
またSample名なども、辞書にして置き、同じ文字列を複数出させないようになっている等の工夫も入っています。
また、データがいわゆる「ステートレス」にはなっておらず、これまでに読んできたデータを利用しないと読み解けないようになっています。
そしてデータを特定のフレームだけ切り出すとかをしようとすると、特定領域だけが抜き出す形ではなく、頭から総なめして必要そうなデータを抜き出すなどの処理が必要になります。
(データを読む処理大変でした)
.data形式について
ProfilerWindowから保存して書き出した形になっています。
どのSampleに何ms掛かったかという事が集計済みの状況ですし、またSample名はそのまま文字列で書き出しています。
こちらの方が読み込むのが楽です。
そのままデシリアライズして、終了という感じで済みますので…
また1フレーム毎にデータが独立していて、切り出しやすいという特徴もあります。
詳細説明
詳細は、こちらの講演スライドのP73~P79をご覧ください。
https://learning.unity3d.jp/1365/
Unityのバージョンとバイナリのデータフォーマットについて
Unityのバージョンがあがる毎に大体バイナリフォーマットが変わってきます。
なので、Unityの新しいバージョンごとに読み込むプログラムを対応する必要になってきます。
特に.data形式はstructをそのままバコーンと放り込むような感じになっていましたので、何か少しでもデータが追加される都度にデータがずれてすぐに壊れてしまいます……。
対して.raw形式はブロックごとにチャント分割されていて、新しいブロックを読めなくても成立することが多いのでまだよいです…。
ちなみに実装の方ですが…。
複数のバージョンに対応するために、大分 場合分けの列挙の嵐です…
※単純に ifじゃなくて Reflectionを利用したデシリアライズになっていて、変数のAttributeに対応するUnityのバージョンを記述するなどの工夫はしていますが…。
で、結局どうだったの?
さっくりとオーバービューするには、csv化してよかったと思います。
GCAllocをしている部分なんかを探すときは、ツールに掛ければGC Alloc一覧をcsvに出してくれますし…
ざっくりとゲーム全体を通して、どの部分(Sample)が重いか何かもわかって、どこに注意をするべきかという点についてもわかりますし…
何よりも、「どの部分が重いですよね?」というのを他の方々と共有する際には csv化していると、データを渡しやすくて楽でした。
またバイナリフォーマットを理解したので、下記の様なバイナリログを分割するツールなども作る事が出来ました。
https://github.com/wotakuro/ProfilerBinarylogSplit
他にも、Profilerの知らなかった仕様も内部フォーマットから知る事が出来たりしました。
例えばProfielrWindow上でTimelineでSample名をクリックすると、関連するオブジェクトが選択されるという仕様があるのですが…
これは Profilerのバイナリを解析している過程で Sampleを保存するときに、SampleWithInstanceという項目があり、実際のSampleとオブジェクトが紐づけが行われる仕様があり、これによってこういう事が出来るようになっているとわかったり…
他にもSampleWithMetadataという形でSampleに対して好きなデータを紐づけることが出来るようなフォーマットになっているとわかっているので、新しいAPI「EmitFrameMetaData」というのが来た時も、あーココにデータ埋め込んでるのねと言うのがわかったり…
バイナリのシリアライズのC++コードをみて、C#への移植は大変でしたがやってよかったと思います。
今後とかとか…
バージョンごとにフォーマットを読んで対応していく日々なんですが…
今後のProfilerフレーム制限問題が何とかなると良いなぁという事も見越して…、バイナリを直接読む以外に、APIを利用してProfilerWindow経由でデータ取ってくるというのも追加したいと思っています。
そして、あわよくばデータの整合性チェックをもう少し仕組化したいと思っています。
(今はcsv書き出ししてみて、その後に目視確認という状況なので…)