ビルドトレースツールの制作 の1 - 準備

諸般の事情でnmosh製のビルドトレースツールをネイティブコードで書き直すことにした。週に10万回と起動されるツールなので、オーバーヘッドと安定性がそろそろ気になってきた。

ビルドトレースとは何か?

ビルドトレースとは、要するに"プロダクトのビルド中に実行されるコンパイラやリンカの起動コマンドライン後から再生できるように収集する"ことで、個人的にソフトウェア品質管理手法として注目している。
一般的には、ビルドトレースはビルドシステムに組込まれて使われる。MSBuildに最近搭載されたBinaryLog https://github.com/Microsoft/msbuild/wiki/Binary-Log はその一例で、ビルドログを構造化されたログフォーマットに保存することができる。
Coverityのような静的解析ツールは、既存のコンパイラ起動プロセスを乗っ取ってビルドログを取得し、後から再生することで実際の静的解析を実装している。同様のものには、compile_commands.jsonを出力するBear https://github.com/rizsotto/Bear が有る。これらは、LD_PRELOADやその他のhook手法を使って実際の乗っ取りを実装している。
ビルドトレース自体を直接的にサポートしたビルドシステムも存在する。CMakeには CMAKE_EXPORT_COMPILE_COMMANDS、CMAKE_C_COMPILER_LAUNCHER や RULE_LAUNCH_COMPILEが有り、これらはビルド時に各種executableの実行をhookしたり、ビルドシステムにcompile_commands.jsonを出力させることができる。ただし、これはビルドシステム全体がCMakeで統一されていなければ意味が無い。(また、CMakeには既にserver modeが存在しビルドに関するメタデータの取得はそちらが推奨されつつある)
制作中のゲームの先代ビルドインフラ(impulse)ではMake/Ninjaの互換実装を用意し、そこでトレースを実施する方法を取った。互換実装では差分ビルドを実装せず、常にビルドを実行し全ゴールを到達させている。この互換実装のメンテナンスが工数的に難しい(& 内製のMakefile生成ツールが生成したMakefileしか処理できない)ので、もうちょっとシンプルなアーキテクチャでビルドシステムおよびトレースを実現しようというのが根底にある。
(ビルドシステムは基本的にCMakeを使用するが、UnityやUEのように自前のパッケージング方法論を持つツールとの統合が難しいのが課題として浮上してきたという事情がある。)
ビルドトレースのキモはトレースの実施自体ではなく、トレースデータを活用する側にある。しかし、意外とトレース取得自体にも良い考察が無いのが現状と言える。我々は普段GitHubなりなんなりでプロジェクトのソースコード自体は公開しているが、本来はビルドトレースとその分析結果も同時に公開されているべきで、それが有ると無いとではコード理解の効率が段違いになる(トレースが無い場合、手元でビルドしないと実際のビルド結果を入手できない)。良いエコシステムを構築するため、トレースによって取得されたログや分析の標準フォーマットを設計することがこの計画の重要な目的と言える。

必要なもの

今回実装しようとしているビルドトレースは次のように図解できる。

ポイントは"既存のビルドシステムを変更する必要が無い"点で、従来は専用のビルドツールを使用していたトレースの取得を、通常のビルド成果物の取得と同時に、そのままのビルドツールで実施できる点が重要と言える。
必要なものは、図中のbuild-tracer.exeやgcc.exe、ld.exeに相当する実行可能形式ファイル1つということになる(1つの実行可能ファイルをリネームして/binに配備する - ただしWindows環境ではABIを元のビルドシステムに合わせる必要があるためWin32とWin64の両者が必要)。
これに要求される機能はそれなりにあり、ネイティブコードで実装するのがちょっと面倒で今迄のプロトタイプではnmoshで書いたスクリプトを使っていた。しかし、これだとWin32/64のケースのように上手くいかない場合があるのと、nmosh自体が稀に起動失敗する症状が有り追うのが面倒なのでネイティブコード化を進めることにした。
必要な機能としては:

  1. 自分自身のファイル名を取得する機能。実はPOSIX的には自分の実行ファイル名を取得するための簡単な方法は無く、nmoshでは各OS毎に実装が存在する。https://github.com/okuoku/mosh/blob/c632378031e16fd3fa059087a30ba04055c54656/src/nmosh/win32/process.c#L1906
  2. 標準入出力とエラー出力をリダイレクトしつつ子プロセスを起動する機能。これはPOSIXであれば大抵の環境にpipeとposix_spawnが有り、Windowsでは専用の実装を用意することになる。POSIXではfdをdupして渡すといった処理を行わせる必要があるが、Win32では直接HANDLEの継承を使うという微妙な違いがある。このため、nmoshではリダイレクトとプロセス生成は一発で行うAPIとして用意していた。https://github.com/okuoku/mosh/blob/c632378031e16fd3fa059087a30ba04055c54656/src/nmosh/win32/process.c#L142
  3. 環境変数の取得と設定。これもPOSIXWindowsの2実装で十分。https://github.com/okuoku/mosh/blob/c632378031e16fd3fa059087a30ba04055c54656/src/nmosh/win32/process.c#L1918
  4. カレントディレクトリの取得と設定。getcwd(2)やchdir(2)は地味にunistd.h上にあるため、Windows上では普通に実装することになる。https://msdn.microsoft.com/en-us/library/windows/desktop/aa364934.aspx
  5. 引数の解析とレスポンスファイルの回収。意外と自明でないが、Win32上ではシェルに渡せるコマンドライン長にかなり制約があり、レスポンスファイルを使用してプログラムが起動されるケースが有る。このレスポンスファイルはテンポラリディレクトリに作成されビルド後に消去される可能性が有るので、コマンドが終了するよりも前に回収しておく必要がある。悪いことに、レスポンスファイルはネストする可能性がある(手元のビルドでは発生していないように見えるが。。)。プロセスに渡されたコマンドライン文字列は、POSIXでは起動時に即回収する必要があるが、Win32ではGetCommandLine APIでいつでも取得できる。Win32の方が柔軟性のある比較的珍しい例。https://msdn.microsoft.com/en-us/library/windows/desktop/ms683156.aspx
  6. 名前付pipeの作成と接続。これもPOSIXWindowsの2実装で十分。POSIXではselectなりpollを使って待合せすることになるが、Windowsには良い代替が無いので素直にIOCPを使うことになる。https://github.com/okuoku/mosh/blob/c632378031e16fd3fa059087a30ba04055c54656/src/nmosh/win32/process.c#L292
  7. timestampの取得。まぁamd64でしか使わないので普通にrdtscでも良いが。。高速なタイムスタンプとgettimeofdayの2通りが必要。今回タイムスタンプには"カルチャ"を持たせ、ログファイルのパース等を行う側で実時間に変換する方針とする。

さらに、結果をシリアライズしてpipeに流すためのシリアライザが必要になる。cmp( https://github.com/camgunz/cmp )とかmpack( https://github.com/ludocode/mpack )のようなMessagePack実装を持ってきても良いかなと思ったけど、この手のツールはパブリックドメインにしておかないと後々面倒なので独自フォーマットを自作することにした。MessagePackなりJSONなりが必要な場合は、ログをさらに処理して取得することになる。