LoginSignup
9
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-11-06

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

元の.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のフィルタは OperationProcess CreatePath に Unityのインストールパスを設定する。

SnapCrab_NoName_2018-11-7_4-30-29_No-00.png

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

トレース対象のリスト
    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 の実装

... 説明しないといけないようなポイントは特にない(コメントに書かれているイベント種の大半は未実装になっている)。単に 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も適当に用意することで回避できた。

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で適当にスクリプトを書いた

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

トレースの観察

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

SnapCrab_NoName_2018-11-7_5-54-22_No-00.png

mono

monoはトレースされていないプロセスから起動されていた。UnityはC#プロジェクトについてはmsbuildを経由してビルドしていたと思うので、msbuildもトレースが必要と思われる。

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

il2cpp

残念ながら今回のトレースではil2cppの動作はよく観察できなかった。トレース中でil2cpp.exeの起動が確認できたのは一度だけで、特に各*.csファイル毎に変換をしているような様子は無かった。

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

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

(ログファイルのフィールド IDNP は自分を呼び出した親の IDNT が含まれている -- 環境変数経由でデータを渡している)

SnapCrab_NoName_2018-11-7_5-22-26_No-00.png

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

emcc

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

  1. emccの呼び出し。普通のCツールチェーンと代わらないオプションを受ける。
  2. emcc → clang++。EMSCRIPTENのバージョンを現わすdefineが追加されているのが観察できる。 オプション emit-llvm が付与されていることから、 ココではJavaScriptではなくLLVM bitcodeを出力している ことに注意する。
  3. emcc → opt。何故かいくつかの最適化は明示的に無効化されている。

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

リンク (LLVM bitcode)

リンクはil2cppがllvm-linkを呼び出して実施する ここで指定されている .bc ファイルが、事前にビルドされたUnityエンジン本体部分に相当する(最初のil2cppの実行から同じファイル群が指定されているのがわかる)。

ここで生成された.bcはoptコマンドで更に他のライブラリと結合された上で最適化される。結合されているライブラリのうちいくつかは、llvm-arコマンドで抽出されてから使用されている(何故?)。

JSとWASMの生成

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

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

  1. llcを使用してLLVM bitcodeを.asm.js形式のJavaScriptに変換する
  2. asm2wasmを使用して.asm.jsをWASMに変換する

asm2wasm はLLVMではなくBinaryenのツールとして実装されている。直接llcからwasmを出力しないのは単に歴史的経緯な気はする。

感想

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

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

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

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5