Unityでコンパイル時間を可視化する

f:id:hirasho0:20190205124546p:plain

Unityでゲームを動かしてみるのに、ビルドを待つ必要はありません。 ソースコードのコンパイルさえ済めば、すぐに動かしてみることができます。

しかし、この時間でさえも、コード量が増えていけば耐え難いものになるのです。

この記事では、技術部平山が、 コインパル時間の可視化を行ったことについて書きます。 また、実際どれくらいコードがあるとどれくらい遅いのか、 ということも調べてみました。

まず測定

コンパイル中かどうかはEditorApplication.isCompiling で取れます。 あとはテキトーなエディタ拡張ウィンドウを作って、表示します。

public class CompileTimer : EditorWindow
{
    [MenuItem("Kayac/CompileTimer")]
    static void Init()
    {
        EditorWindow window = GetWindowWithRect(typeof(CompileTimer), new Rect(0, 0, 200f, 100f));
        window.Show();
    }

    // コンパイル前後で変数を保持できないのでEditorPrefsに入れる必要がある
    const string compileStartTimeKey = "kayac_compileStartTime";
    const string lastCompileTimeKey = "kayac_lastCompileTime";

    void OnGUI()
    {
        DateTime startTime = new DateTime();
        var compiling = EditorApplication.isCompiling;
        bool prevCompiling = false;
        if (EditorPrefs.HasKey(compileStartTimeKey))
        {
            string str = EditorPrefs.GetString(compileStartTimeKey);
            if (!String.IsNullOrEmpty(str))
            {
                startTime = DateTime.Parse(str);
                prevCompiling = true;
            }
        }
        float lastTime = 0f;
        lastTime = EditorPrefs.GetFloat(lastCompileTimeKey);
        var currentTime = 0f;
        if (prevCompiling)
        {
            if (compiling)
            {
                currentTime = (float)(DateTime.Now - startTime).TotalSeconds;
            }
            else
            {
                lastTime = (float)(DateTime.Now - startTime).TotalSeconds;
                EditorPrefs.SetFloat(lastCompileTimeKey, lastTime);
                EditorPrefs.DeleteKey(compileStartTimeKey);
            }
        }
        else if (compiling)
        {
            EditorPrefs.SetString(compileStartTimeKey, DateTime.Now.ToString());
        }
        else if (EditorPrefs.HasKey(compileStartTimeKey))
        {
            EditorPrefs.DeleteKey(compileStartTimeKey);
        }
        EditorGUILayout.LabelField("Compiling:", compiling ? currentTime.ToString("F2") : "No");
        EditorGUILayout.LabelField("LastCompileTime:", lastTime.ToString("F2"));
        this.Repaint();
    }
}

コードはプロジェクトごとgithubに置いてあります。 OnGUIが呼ばれる度にisCompilingを見て、「前コンパイル中で、今コンパイルしていなければ」 終了とみなします。開始時刻は保存しておき、今の時刻から引けば時間がわかります。

そんな簡単な処理の割には長いのは、コンパイルの前後で変数を保持できないからです。 _compileStartTimeみたいな変数を持っておいても、コンパイル終了時にクラスインスタンスが 作り直されて「西暦1年1月1日」になってしまいます。 なので、保存するものは全部EditorPrefsにセーブしているわけです。

OnGUIのためにToString()によるGCAllocが走るのが気に入りませんが、 それはまたそのうち考えます(変数が保持される間はキャッシュしておけば良いでしょう)。

かくして、最後のコンパイル所要時間を表示しておきつつ、 冒頭のようにコンパイル中はコンパイル経過時間を表示する、 というエディタ拡張ができました。

コンパイルは何をやったら遅いのか

結論から言えば、単純に量です。 クラス間関係の数とか、partialとか、まあいろいろ試しましたが、あんまり関係ないように見えます。

例えば、今回用意したサンプルプロジェクトは4-6秒でコンパイルが終わります。 コード量はゲームの製品に比べればほぼゼロですが、最低でもこれくらいかかってしまうようです。 測定は MacBook Pro Mid-2014にて行いました。

ここに、今回用意したスクリプト で、1000個のメソッドを持つクラスを1000個生成すると(GenerateHuge)、コード行数は100万行程度になり、 コンパイル時間は65秒前後になります。こうなると相当に辛い状況です。 Debug.Logをはさんで再実行する度に1分待たされます。

できるだけコードでなくAssetBundleに入れられる形のデータで物を足す、 というのが、理想的な解決策でしょう。量産前にその体制を作れるかどうかは、 相当に大事なことであると思います。 それを怠ると、コードはじわじわと増えていき、気づいた時にはもう手遅れになってしまうのです。 今回の可視化ツールは、それを防ぐ助けになるかもしれません。 できれば専用のウィンドウを出さずに表示するように変えたいものです。

もちろん、できてしまった物をどうにかする方法もあるにはあり、 dllを分ける(2017.3以降)Pluginsの下に入れる、 といった手法もあります。 100万行超えてしまってさすがに辛い、となれば、これらを駆使して改善する必要があります。

しかし、できればそうなる前に手を打ちたいものです。

おまけ

せっかくなので、プロジェクトのコード量を測定して、 どのフォルダ以下にどれくらいコードがあるのかわかる機能もつけておきました。 「コード量分析」ボタンがありまして、

f:id:hirasho0:20190205124546p:plain

押すとAssetsフォルダがある所にcodeAmount.txtが生成されます。 例として弊社東京プリズンの結果 をご覧ください。単位はバイトで、ほぼ文字数と考えて良いかと思います。 ご自分の製品と比べてみてください。

ちなみに、東京プリズンにおいては、自動で生成されたコードがそこそこあります。例えば、 AfterEffectsからの変換 で生成されたものも結構あり、コンパイル時間の面では良くありません。 単純に再生して終わりのアニメーションは、できるだけコードでない方法で実装したいものです。