Unityプロファイラーウィンドウで負荷の高いフレームを抽出する

# はじめに
Unityのプロファイラーウィンドウ上で負荷の高い箇所を抽出し、選択してくれる機能を作成してみました。
![HeavyFrames.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/d76ca1ea-eaac-e2ef-bf5e-a7cb3fd7a9a9.gif)

# 環境
- 確認バージョン
    - `Unity2019.4.13f1`
    - `Unity2020.1.10f1`
- Windows

# ウィンドウ
Refreshボタンを押すと開いているプロファイラーの情報から重いフレームを抽出して `<` と `>`ボタンで
重いフレームを選択できるようになっています。
![2020-12-19_16h39_41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/67302011-880d-7a13-f91d-30b22f2d3279.png)

# 負荷が高いところの抽出
プロファイラーウィンドウでスクリプトから `Time ms`の高いところを抽出します。

```cs
　　　　 var property = new ProfilerProperty();
        var firstIndex = ProfilerDriver.firstFrameIndex;
        var lastIndex = ProfilerDriver.lastFrameIndex;
        for (var index = firstIndex; index < lastIndex; index++)
        {
            property.SetRoot(index, HierarchyFrameDataView.columnSelfTime, 0);
            var value = property.GetColumnAsSingle(HierarchyFrameDataView.columnTotalTime);
            if (value > thresholdField.value)
            {
                 // 負荷が高い
            }
        }
```

負荷が高いかどうかの判断については PlayerLoopのTime(ms)がしきい値(Threshold(ms))を超えている箇所としています。
`ProfilerProperty`を上記のように使うことで負荷がとれることができます。

# コード全体

コード全体になります。UIElementsを使用しています。
[Gist](https://gist.github.com/t-macnaga/236ea535534000159f434e33ffa199b0)にもアップしましたので使っていただければと思います。

```ProfilerHeavyFrameCaptureWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEditor.Profiling;
using UnityEditor.UIElements;
using UnityEditorInternal;

/// <summary>
/// Profilerの情報を取得するウィンドウ
/// </summary>
public class ProfilerHeavyFrameCaptureWindow : EditorWindow
{
    class HeavyFrame
    {
        public int frameIndex;
        public string label;
    }
    List<HeavyFrame> heavyFrames = new List<HeavyFrame>();
    int heavyIndex;
    FloatField thresholdField;
    Label countLabel;

    [MenuItem("Window/ProfilerHeavyFrameCapture")]
    static void ShowWindow()
    {
        GetWindow<ProfilerHeavyFrameCaptureWindow>();
    }

    void OnEnable()
    {
        var toolbar = new Toolbar();
        toolbar.style.minHeight = 20F;
        rootVisualElement.Add(toolbar);

        var prevButton = new ToolbarButton(() => Prev());
        prevButton.text = "<";
        prevButton.style.minWidth = 100F;
        toolbar.Add(prevButton);

        var nextButton = new ToolbarButton(() => Next());
        nextButton.text = ">";
        nextButton.style.minWidth = 100F;
        toolbar.Add(nextButton);

        countLabel = new Label();
        countLabel.text = "";
        toolbar.Add(countLabel);

        var refreshButton = new ToolbarButton(() => Refresh());
        refreshButton.text = "Refresh";
        toolbar.Add(refreshButton);

        thresholdField = new FloatField("Threshold(ms)");
        thresholdField.viewDataKey = "threshold";
        thresholdField.value = 20F;
        rootVisualElement.Add(thresholdField);
    }

    void SetCurrentFrame()
    {
        var frameIndex = heavyFrames[heavyIndex].frameIndex;
        SetCurrentFrameOfProfilerWindow(frameIndex);
        UpdateCountLabel();
    }

    void UpdateCountLabel()
    {
        countLabel.text = $"{heavyIndex + 1}/{heavyFrames.Count}";
    }

    void Prev()
    {
        heavyIndex--;
        heavyIndex = Math.Max(heavyIndex, 0);
        SetCurrentFrame();
    }

    void Next()
    {
        heavyIndex++;
        heavyIndex = Math.Min(heavyIndex, heavyFrames.Count - 1);
        SetCurrentFrame();
    }

    void Refresh()
    {
        CaptureHeavyFrames();
        UpdateCountLabel();
    }

    void CaptureHeavyFrames()
    {
        heavyFrames.Clear();
        var property = new ProfilerProperty();
        var firstIndex = ProfilerDriver.firstFrameIndex;
        var lastIndex = ProfilerDriver.lastFrameIndex;
        for (var index = firstIndex; index < lastIndex; index++)
        {
            property.SetRoot(index, HierarchyFrameDataView.columnSelfTime, 0);
            var value = property.GetColumnAsSingle(HierarchyFrameDataView.columnTotalTime);
            if (value > thresholdField.value)
            {

                heavyFrames.Add(new HeavyFrame { frameIndex = index, label = index.ToString() });
            }
        }
    }

    /// <summary>
    /// プロファイラーウィンドウを指定のframeIndexが選択状態にします。
    /// </summary>
    void SetCurrentFrameOfProfilerWindow(int frameIndex)
    {
        var profiler = Resources.FindObjectsOfTypeAll<EditorWindow>()
        .FirstOrDefault(x => x.GetType().Name == "ProfilerWindow");
        var setCurrentFrame = profiler.GetType().GetMethod("SetCurrentFrame", BindingFlags.NonPublic | BindingFlags.Instance);
        setCurrentFrame.Invoke(profiler, new object[] { frameIndex });
        profiler.Repaint();
    }
}
```

# 最後に
負荷の高い箇所を楽に選択できればいいなとおもい作成したエディタ拡張ではありましたがいかがでしょうか。
プロファイリングのタスクで重宝すればと思います。

# 参考資料
- [UnityのProfilerWindowの情報をスクリプトで取得する](https://qiita.com/KTA552/items/7d6abbd2224e04b77bde)
    - ProfilerPropertyの使い方参考にさせていただきました。