Unity WebGLの構成をビルドトレースから眺める

Unityの動作プラットフォームにはiOSやWindowsに混ざってWebGLもある。今回はビルドに使用されるコマンドラインをトレースして、どのようにWebGLビルドが作成されているのかを追跡してみた。

UnityのソースコードはPersonal契約では入手できないが、WebGL版のビルドに使用されるLLVM-IR(中間言語)はほぼソースコードのようなものなので、WebGLビルドをソースコードに戻すことはそれほど難しくないと考えている。

(使用したUnityのバージョンは `2018.2.14f1`)

※ **DO NOT TRY THIS AT HOME。作成したツールは真面目に作っているわけではないので、たぶん他所では動作しない。** ビルドトレース自体は興味深い手法だが、汎用性を持たせるのが結構難しい。。そのうちビューア等と一緒に纏める予定。

# ビルドトレース

ビルドトレースは個人的に愛用しているソフトウェア分析手法で、ソフトウェアをソースコードからビルドする際に実行されるコマンドを分析し、必要に応じてパラメタを変更したりコンパイラを差し替えてリプレイすることでソフトウェアの情報を抽出することを目的としている。

ビルドトレースの手法自体はclangの `scan-build` やCoverityのような静的解析製品で広く採用されている。しかし、プラットフォームとして整備されているものはあまり見当らず、製品毎に専用のトレース手法を用意しているのが現状と言える。

今回は、侵襲性の低い "**.exeファイル置き換え**" 手法によってビルドトレースを実現する。

![SnapCrab_NoName_2018-11-14_4-52-27_No-00.png](https://qiita-image-store.s3.amazonaws.com/0/67446/8a2d79e9-686d-ab4a-a61e-cca40f837e51.png)

元の.exeファイルはリネームして残しておき、置き換えられた.exeは引数やレスポンスファイルをログとして適当なディレクトリに書き出したあと、元の.exeファイルを実行する。この手法により、.exeをフックしたり、Unity自体に手を入れなくても、詳細なビルドログを入手できる。

.exeのフック(APIレベルトレース)やカーネルレベルのトレースの方が簡単に実装できる反面、Windows Containerやアンチウイルス等との相性が不味いという問題がある。

# トレースの実施

Unityで適当にプロジェクトを用意し、ビルドを実行できるようにする。この辺はたぶん大量に文章が有るので割愛。

## procmonを使用した事前分析

まず、トレースを行う対象を抽出するためにprocmon ( https://technet.microsoft.com/ja-jp/sysinternals/processmonitor.aspx )を実行しつつ普通にビルドを実施する。Unityはビルド結果を積極的にキャッシュするため、プロジェクトをクリーンビルドする必要がある。クリーンする方法はよくわからなかったので、プロジェクトのディレクトリを `git clean` で毎回初期状態に戻している。

procmonのフィルタは `Operation` に `Process Create` 、 `Path` に Unityのインストールパスを設定する。

![SnapCrab_NoName_2018-11-7_4-30-29_No-00.png](https://qiita-image-store.s3.amazonaws.com/0/67446/8e4f63e6-5268-3af6-5b8a-9237dc8065ae.png)

適当にトレース結果を眺めることにより、今回は以下のexecutableがトレース対象ということがわかった。

```cmake:トレース対象のリスト
    Editor/Data/Tools/InternalCallRegistrationWriter/InternalCallRegistrationWriter.exe
    Editor/Data/Tools/FSBTool/x64/FSBTool64.exe
    Editor/Data/Tools/FSBTool/x86/FSBTool.exe
    # Blacklist UnityShaderCompiler -- generates 0 bytes logfile
    # Editor/Data/Tools/UnityShaderCompiler.exe
    Editor/Data/Mono/bin/mono.exe
    Editor/Data/MonoBleedingEdge/bin/mono.exe
    Editor/Data/il2cpp/build/UnityLinker.exe
    Editor/Data/il2cpp/build/il2cpp.exe
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten_Win/python/2.7.5.3_64bit/python.exe
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten_FastComp_Win/llvm-link.exe
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten_FastComp_Win/llvm-ar.exe
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten_FastComp_Win/llc.exe
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten_FastComp_Win/binaryen/bin/asm2wasm.exe
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten_FastComp_Win/llvm-nm.exe
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten_FastComp_Win/opt.exe
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten_FastComp_Win/clang.exe
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten_FastComp_Win/clang++.exe
```

ここで、 **UnityShaderCompiler は除外している** 。Unityは時間の掛かるシェーダコンパイルを高速化するために専用のワーカープロセスを用意しており、これらはコマンドラインでパラメタを受けない。このため、 .exeの置き換えでは正常にトレースを実施できない。今回はCPUコードにだけ着目しているので一旦無視することにした。

## 置き換え .exe の実装

https://github.com/okuoku/rs-buildreplay-proto/blob/1341fa4872a2581a28c814723056cf772c6beb80/execlogger_win32.c

... 説明しないといけないようなポイントは特にない(コメントに書かれているイベント種の大半は未実装になっている)。単に `argv` を受け、加工して `CreateProcess` し、結果を合わせてreturnするだけでOK。

細かいポイント:

1. Windowsはプロセスを作成する際のコマンドライン引数長制限が激烈に厳しかったため、いわゆる **レスポンスファイル** がビルド中に普通に使用されている。Visual Studio系のツールが **UTF-16のレスポンスファイルを使用する** ため、ツールはレスポンスファイルをUTF-8に変換して記録する機能を持っている。
2. コンパイラの中には .exe のファイル名をリネームすると正常に動作しないものが有る。このため、配置オプションとしてサブディレクトリ `rs-exectrace` 内に `rs-exectrace/clang.exe` のように配置しても良いことにした。(もっとも、Unityではこの必要はなかった。)
3. トレースファイル名としてPIDだけではなく、 `QueryPerformanceCounter` APIで取得したモノトニック時刻も含めている。 **Windowsではかなり頻繁にPID番号が再利用される傾向にある** ようなので、時刻を含めることでトレースファイル名のユニーク性を確保している。

また、最初のバージョンはmonoを正常にトレースすることができなかった。これは、Monoがstdinを何故かopenしようとしていたことが原因で、[Stdinも適当に用意する](https://github.com/okuoku/rs-buildreplay-proto/commit/1341fa4872a2581a28c814723056cf772c6beb80)ことで回避できた。

```:monoの実行失敗
Filename: F:\unity\u2018.2.14f1-ARMED\Editor\Data\MonoBleedingEdge\bin\mono.exe
Arguments: F:/unity/u2018.2.14f1-ARMED/Editor/Data/Tools/ScriptUpdater/AssemblyUpdater.exe  --timestamp 636769625579347308  --api-version 2018.2.14f1 --check-update-required -a C:/Users/oku/AppData/Local/Unity/cache/packages/packages.unity.com/com.unity.ads@2.0.8/UnityEngine.Advertisements.dll -s "F:\unity\u2018.2.14f1-ARMED\Editor\Data\Managed,+F:/unity/u2018.2.14f1-ARMED/Editor/Data\UnityExtensions/Unity,+C:/Users/oku/StandardAssetsTest/Assets" F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\GUISystem\UnityEngine.UI.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\GUISystem\Standalone/UnityEngine.UI.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\GUISystem\Editor/UnityEditor.UI.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\TestRunner\Editor/UnityEditor.TestRunner.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\TestRunner\UnityEngine.TestRunner.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\TestRunner\net35/unity-custom/nunit.framework.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\TestRunner\portable/nunit.framework.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\Timeline\RuntimeEditor/UnityEngine.Timeline.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\Timeline\Runtime/UnityEngine.Timeline.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\Timeline\Editor/UnityEditor.Timeline.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\TreeEditor\Editor/UnityEditor.TreeEditor.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\Networking\UnityEngine.Networking.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\Networking\Standalone/UnityEngine.Networking.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\Networking\Editor/UnityEditor.Networking.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnityGoogleAudioSpatializer\Editor/UnityEditor.GoogleAudioSpatializer.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnityGoogleAudioSpatializer\Runtime/UnityEngine.GoogleAudioSpatializer.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnityGoogleAudioSpatializer\RuntimeEditor/UnityEngine.GoogleAudioSpatializer.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnityHoloLens\Editor/UnityEditor.HoloLens.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnityHoloLens\Runtime/UnityEngine.HoloLens.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnityHoloLens\RuntimeEditor/UnityEngine.HoloLens.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnitySpatialTracking\Editor/UnityEditor.SpatialTracking.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnitySpatialTracking\Runtime/UnityEngine.SpatialTracking.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnitySpatialTracking\RuntimeEditor/UnityEngine.SpatialTracking.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\UnityExtensions\Unity\UnityVR\Editor/UnityEditor.VR.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\Managed\UnityEngine.dll F:\unity\u2018.2.14f1-ARMED\Editor\Data\Managed\UnityEditor.dll
index: -1
Trying F:\unity\u2018.2.14f1-ARMED\Editor\Data\MonoBleedingEdge\bin\rs-exectrace\mono.exe


Unhandled Exception:
System.TypeInitializationException: The type initializer for 'System.Console' threw an exception. ---> System.ArgumentException: handle

Parameter name: Invalid.
  at System.IO.FileStream..ctor (System.IntPtr handle, System.IO.FileAccess access, System.Boolean ownsHandle, System.Int32 bufferSize, System.Boolean isAsync, System.Boolean isConsoleWrapper) [0x0002d] in <f2e6809acb14476a81f399aeb800f8f2>:0 
  at (wrapper remoting-invoke-with-check) System.IO.FileStream..ctor(intptr,System.IO.FileAccess,bool,int,bool,bool)
  at System.Console.Open (System.IntPtr handle, System.IO.FileAccess access, System.Int32 bufferSize) [0x00000] in <f2e6809acb14476a81f399aeb800f8f2>:0 
  at System.Console.OpenStandardInput (System.Int32 bufferSize) [0x00005] in <f2e6809acb14476a81f399aeb800f8f2>:0 
  at System.Console.SetupStreams (System.Text.Encoding inputEncoding, System.Text.Encoding outputEncoding) [0x0005d] in <f2e6809acb14476a81f399aeb800f8f2>:0 
  at System.Console..cctor () [0x0008e] in <f2e6809acb14476a81f399aeb800f8f2>:0 
   --- End of inner exception stack trace ---
  at AssemblyUpdater.Application.Program.Main (System.String[] args) [0x00072] in <9b15f78862324bb78e7634166b702b09>:0 

[ERROR] FATAL UNHANDLED EXCEPTION: System.TypeInitializationException: The type initializer for 'System.Console' threw an exception. ---> System.ArgumentException: handle
```

## トレースの実施

トレースの準備は .exe で各トレース対象を置き換え、本来のコンパイラやツールを `clang.exe.traced.exe` のようにリネームして残すことで行える。今回は [CMakeで適当にスクリプトを書いた](https://github.com/okuoku/rs-buildreplay-proto/blob/1341fa4872a2581a28c814723056cf772c6beb80/cmake/RsBuildReplayUtil.cmake#L17)。

Unityのインストールディレクトリを書き換えることになるので、[コマンドラインからインストールするなりなんなり](https://docs.unity3d.com/ja/current/Manual/InstallingUnity.html) で専用のインストールディレクトリを用意する必要がある。また、ビルドだけであれば単にエクスプローラを使用して `Unity/Editor` ディレクトリをコピーするだけでも正常に動作する。

# トレースの観察

[取得したトレースデータのうち興味深いものはGistに上げておいた](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef)。実際のトレースは 2000 ファイル近くあり、つまり1つのWebGLプロジェクトをビルドするのに少くとも2000回は.exeの起動が行われていることになる。

![SnapCrab_NoName_2018-11-7_5-54-22_No-00.png](https://qiita-image-store.s3.amazonaws.com/0/67446/f6c68932-afca-32c8-d0d2-60dc5dce68d0.png)

## mono

[monoはトレースされていないプロセスから起動されていた](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229e8e3db57-207956-bin-txt)。UnityはC\#プロジェクトについては`msbuild`を経由してビルドしていたと思うので、`msbuild`もトレースが必要と思われる。

...ちょっと後まわし。

## il2cpp

残念ながら今回のトレースではil2cppの動作はよく観察できなかった。[トレース中でil2cpp.exeの起動が確認できたのは一度だけ](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229a54cffdc-56436-bin-txt)で、特に各\*.csファイル毎に変換をしているような様子は無かった。

il2cppはコマンドラインオプション `--generatedcppdir=C:\Users\oku\StandardAssetsTest\Temp\StagingArea\Data\il2cppOutput` のようにしてディレクトリに各クラスの変換結果を直接出力している。たぶんちゃんと調べれば生成された.cppファイルを保持するようなオプションも有るんじゃないかという気はする。

特に注目すべきなのは、 **il2cppが後段のemccやリンカを駆動している** という事実で、il2cppの直接の子プロセスは514個もある。

(ログファイルのフィールド `IDNP` は自分を呼び出した親の `IDNT` が含まれている -- [環境変数経由でデータを渡している](https://github.com/okuoku/rs-buildreplay-proto/blob/1341fa4872a2581a28c814723056cf772c6beb80/execlogger_win32.c#L187))

![SnapCrab_NoName_2018-11-7_5-22-26_No-00.png](https://qiita-image-store.s3.amazonaws.com/0/67446/3dd21de3-154d-647d-d45c-ccf2f87c131b.png)

このため、UnityのIL2CPPベースプロジェクトについては、il2cppが事実上のビルドツールとして機能していると見て良いのではないかと思う。

## emcc

WebGLビルドではEmscriptenが使用され、そのコンパイラは `emcc` または `em++` と呼ばれる。emccはPythonで書かれたコンパイラドライバで、更に `clang` や `opt` といった各LLVMのツールを呼び出してリンクを実施する。

1. [emccの呼び出し。普通のCツールチェーンと代わらないオプションを受ける。](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229c3727928-155612-bin-txt)
2. [emcc → clang\+\+。EMSCRIPTENのバージョンを現わすdefineが追加されているのが観察できる。](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229c38b27ef-159396-bin-txt) オプション `emit-llvm` が付与されていることから、 *ココではJavaScriptではなくLLVM bitcodeを出力している* ことに注意する。
3. [emcc → opt。何故かいくつかの最適化は明示的に無効化されている。](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229c3b9dc7d-164760-bin-txt)

... ココでOptを実行する理由はちょっと解らなかった。Emscriptenはどうせリンク時に最適化を実施するので、細かい単位での最適化は、無駄なビルド時間の肥大化を抑えるためではないだろうか。

## リンク (LLVM bitcode)

[リンクはil2cppがllvm-linkを呼び出して実施する](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229c7fb2768-195036-bin-txt) ここで指定されている .bc ファイルが、事前にビルドされたUnityエンジン本体部分に相当する(最初のil2cppの実行から同じファイル群が指定されているのがわかる)。

ここで生成された.bcは[optコマンドで更に他のライブラリと結合された上で最適化される](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229cf121061-204104-bin-txt)。結合されているライブラリのうちいくつかは、[llvm-arコマンドで抽出されてから使用されている](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229ced540a0-203524-bin-txt)(何故？)。

## JSとWASMの生成

リンクによってネイティブコード(?)部分が一つのファイル(`build.bc`)にまとまったら、これをEmscriptenのツールを使用してJSやWASMに変換する。

どちらのプロセスもemccから呼ばれる。

1. [llcを使用してLLVM bitcodeを.asm.js形式のJavaScriptに変換する](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229d3a50529-198040-bin-txt) 
2. [asm2wasmを使用して.asm.jsをWASMに変換する](https://gist.github.com/okuoku/ee5f81793656ea5ead8ab921144f6cef#file-rslog-00000229dc720e79-203864-bin-txt)

`asm2wasm` はLLVMではなく[Binaryen](https://github.com/WebAssembly/binaryen)のツールとして実装されている。直接`llc`からwasmを出力しないのは単に歴史的経緯な気はする。

# 感想

- Win32ビルドのトレースでは、やっぱりレスポンスファイルの収集は必須だった。
- リダイレクトについては真面目に実装する必要は無かった - UNIX的なpipeを使われるとダメなので真面目にやらないといけない気持ちは有るけど `gcc` ぐらいでしか必須じゃないし。。
- C\#部分のトレースができないのがちょっと。。
- API hookやカーネルレベルのトレースに比べてDeployの面倒は有るがそれなりに実用的な結果が確認できた。

ゲームのQCにビルドトレースを使うのは前世紀からやっているのでそろそろ20周年で、毎回プロジェクトをやるたびにトレース手法を用意するんではなく、プラットフォームとしての考察をしたいところではある。。

単純な.exe置き換えとログファイルによるトレースの限界としては、生成物がTEMPディレクトリに配置されるようなケースで、ファイルの回収をその場で行えない点がある。この制約を克服するためには真面目なクライアント / サーバ構成にして引数解析とファイルの収集ルールを管理できるようにする必要があるが、面倒。。今回は **レスポンスファイルだけはどうしても必要なので特別扱い** している。