10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

グレンジAdvent Calendar 2021

Day 5

Unity Profilerの表示情報をReflectionで取得してみる

Last updated at Posted at 2021-12-04

こんにちは、グレンジ Advent Calendar 2021の5日目担当のmesshiです。

本記事は、Unity Profiler上に表示されている情報を書き出すための方法について取り上げます。
ここで紹介する方法はProfilerに限らず応用が可能ですので、UnityEditorの表示情報を書き出したいと思っている方は、是非参考にしてみてください。

背景

本記事で取得を試みる情報は、Unity Profiler(Memory)の「Detailedモード」で表示した内容です。
profiler_memory.png

この情報はUnity Editorから保存する方法が提供されていません。
そのため、いちいちテキストに書き起こしたり、スクショを取る必要があります。
しかし、タイポの可能性や複数枚の画像での管理を考慮すると、できれば避けたいところです。
私はこの情報をよく利用していたため、どうにかしてテキストに書き出せないかトライしてみました。

必要な情報を集める

UnityはC#部分のソースコードを公開しています。
https://github.com/Unity-Technologies/UnityCsReference

まずはこのリポジトリ内から、対象と思われるソースが記載されているcsファイルを探しましょう。
今回はProfiler系なので次のフォルダが怪しいと推測できるでしょう。
UnityCsReference/Modules/ProfilerEditor/ProfilerWindow/ProfilerModules/Memory/

あたりが付いたところで、必要な情報を抽出していきましょう.

MemoryElementDataManager

MemoryElementDataManager.cs を覗いて見ると、以下のようなコードがあります

static public MemoryElement GetTreeRoot(ObjectMemoryInfo[] memoryObjectList, int[] referencesIndices)
{
    ...
    MemoryElement root = new MemoryElement();
    System.Array.Sort(allObjectInfo, SortByMemoryClassName);
    root.AddChild(new MemoryElement("Scene Memory", GenerateObjectTypeGroups(allObjectInfo, ObjectTypeFilter.Scene)));
    root.AddChild(new MemoryElement("Assets", GenerateObjectTypeGroups(allObjectInfo, ObjectTypeFilter.Asset)));
    root.AddChild(new MemoryElement("Builtin Resources", GenerateObjectTypeGroups(allObjectInfo, ObjectTypeFilter.BuiltinResource)));
    root.AddChild(new MemoryElement("Not Saved", GenerateObjectTypeGroups(allObjectInfo, ObjectTypeFilter.DontSave)));
    root.AddChild(new MemoryElement("Other", GenerateObjectTypeGroups(allObjectInfo, ObjectTypeFilter.Other)));
    root.children.Sort(SortByMemorySize);

    return root;
}

まさにDetailedで表示している情報の要素がありました。
ここではMemoryElementを返却しています。
返却先はMemoryProfilerModule.csになっています。
return_value.png

MemoryElement

MemoryElement.cs内のデータは次のようになっています

[System.Serializable]
internal class MemoryElement
{
    public List<MemoryElement> children = null;
    public MemoryElement parent = null;
    public ObjectInfo memoryInfo;

    public long totalMemory;
    public int totalChildCount;
    public string name;
    public bool expanded;
    public string description;

抑えておくべきポイントは以下です

  • 子供の情報をリストで保持している
  • 表示されている名前やメモリ量をメンバ変数で保持している

先程のMemoryDataManagerで返却していたRootは、すべての子情報を持っているMemeoyElement情報だったと言えます。
次に、その返却先を追ってみましょう

MemoryProfilerModule

MemoryElementDataManagerで取り上げたGetTreeRoot関数の返却先は、ここで紹介するMemoryProfilerModule.csのSetMemoryProfileInfoメソッドです。

internal class MemoryProfilerModule : ProfilerModuleBase
{
    MemoryTreeListClickable m_MemoryListView;
...
    static void SetMemoryProfilerInfo(ObjectMemoryInfo[] memoryInfo, int[] referencedIndices)
    {
        if (instance.IsAlive && (instance.Target as MemoryProfilerModule).wantsMemoryRefresh)
        {
            (instance.Target as MemoryProfilerModule).m_MemoryListView.SetRoot(MemoryElementDataManager.GetTreeRoot(memoryInfo, referencedIndices));
        }
    }
...
}

処理としては、取得したRootのMemoryElementを、MemoryTreeListClickableのm_MemoryListViewにセットしています。
次に、MemoryTreeListClickableを追ってみましょう。

MemoryTreeListClickable

MemoryTreeListClickableは、MemoryTreeList.cs 内に定義されています.

class MemoryTreeListClickable : MemoryTreeList
{
...
}

MemoryTreeListClickableは、MemoryTreeListを継承していますが、SetRoot関数自体はMemoryTreeListに定義されています。

protected MemoryElement m_Root = null;
...
public void SetRoot(MemoryElement root)
{
    MemoryElement    oldRoot = m_Root;

    m_Root = root;
    if (m_Root != null)
        m_Root.ExpandChildren();
    if (m_DetailView != null)
        m_DetailView.SetRoot(null);

    // Attempt to restore the old state of things by walking the old tree
    if (oldRoot != null && m_Root != null)
        RestoreViewState(oldRoot, m_Root);
}

SetRoot関数の中を見ると、m_Rootというメンバ変数に、表示情報が格納されていることが分かりました

ProfilerWindow

最後に、MemoryProfilerModule自体が誰から参照されているのかを追います。
これはベースクラスであるProfilerModuleBaseが誰から参照されているのかを確認すると、ProfilerWindow.csがありました。
そのソースを見てみると次のように様々なモジュールが格納されていることが分かります。

public sealed class ProfilerWindow : EditorWindow, IHasCustomMenu, IProfilerWindowController, ProfilerModulesDropdownWindow.IResponder
{
...
    [SerializeReference] List<ProfilerModule> m_AllModules;
...
}

これで必要な情報が揃いました

情報の整理

今までの情報を整理してみましょう.

  • ProfilerModuleがMemoryProfilerModuleを保持
  • MemoryProfilerModuleのMemoryTreeListClickableが表示情報のMemoryElementのルート情報保持
  • MemoryElementが子情報と占有メモリ量、オブジェクト名を保持

今回の調査の取っ掛かりとなったMemoryElementDataManagerは不要です。
あとはリフレクションを利用して、必要な情報を取得していくだけです。

リフレクションで取得していく

MemoryProfilerModuleを取得する

まずProfilerWindowの参照を取得します

var profilerWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.ProfilerWindow");
var profilerWindow = EditorWindow.GetWindow(profilerWindowType);

次に、全モジュールを保持しているリストの参照を取得しましょう

var moduleFieldName = "m_AllModules";
var moduleField = profilerWindow.GetType().GetField(moduleFieldName, BindingFlags.NonPublic | BindingFlags.Instance);

そして、リストの中からMemoryProfilerModuleを抽出します

var memoryProfilerModuleType = typeof(EditorWindow).Assembly.GetType("UnityEditorInternal.Profiling.MemoryProfilerModule");
var moduleList = (IList)moduleField.GetValue(profilerWindow);
foreach (var module in moduleList) {
    if (module.GetType() == memoryProfilerModuleType) {
        return module;
    }
}

ここまでのコードをまとめると以下のようになります

    private static object GetMemoryProfilerModule()
    {
        var profilerWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.ProfilerWindow");
        var profilerWindow = EditorWindow.GetWindow(profilerWindowType);
        var moduleFieldName =
        // バージョンごとに微妙に変数名が違う
#if UNITY_2021_2_OR_NEWER
            "m_AllModules";
#elif UNITY_2020_2_OR_NEWER
            "m_Modules";
#else
            "m_ProfilerModules";
#endif
        var moduleField = profilerWindow.GetType().GetField(moduleFieldName, BindingFlags.NonPublic | BindingFlags.Instance);
        if (moduleField == null) {
            // Unity2018はサポート外とする
            Debug.LogWarning("Not Supported Version.");
            return null;
        }

        var memoryProfilerModuleType = typeof(EditorWindow).Assembly.GetType("UnityEditorInternal.Profiling.MemoryProfilerModule");
        var moduleList = (IList)moduleField.GetValue(profilerWindow);
        foreach (var module in moduleList) {
            if (module.GetType() == memoryProfilerModuleType) {
                return module;
            }
        }

        Debug.LogWarning("Not find Memory Profiler Module");
        return null;
    }

MemoryTreeListClicakbleを取得する

先ほど定義した関数を利用して、MemoryProfilerModuleを取得し、リフレクションで対象のオブジェクトを取得します.

var memoryProfilerModule = GetMemoryProfilerModule();
var referenceListViewFieldInfo = memoryProfilerModule.GetType().GetField("m_MemoryListView", BindingFlags.NonPublic | BindingFlags.Instance);
var referenceListView = referenceListViewFieldInfo.GetValue(memoryProfilerModule);

MemoryElementのルート情報を取得する

MemoryTreeListClickableから、同様にメンバ変数を取り出すだけです

var rootMemoryElementField = referenceListView.GetType().GetField("m_Root", BindingFlags.NonPublic | BindingFlags.Instance);
var rootMemoryElement = rootMemoryElementField.GetValue(referenceListView);

MemoryElementから情報を抜き出す

ここでNameとTotalMemoryだけを抜き出す例を記載します

var memoryElementType = typeof(EditorWindow).Assembly.GetType("UnityEditor.MemoryElement");

var nameFiled = memoryElementType.GetField("name", BindingFlags.Instance | BindingFlags.Public);
var name = (string)nameFiled.GetValue(rootMemoryElement);

var totalMemoryField = memoryElementType.GetField("totalMemory", BindingFlags.Instance | BindingFlags.Public);
var totalMemory = (long)TotalMemoryField.GetValue(rootMemoryElement);

あとは、Root情報から子情報を抜き出していけば、Memory Profilerで取得した情報のデータが構築できます
テキストで書き出すまでの最終形のコードを参考までに最後に記載しておきますので、是非参考にしてください。

まとめ

本記事では、次のような流れで情報を取得しました

  • UnityのC#コードのリポジトリが変数名や関係性を把握する
  • リフレクションで情報を取得していく

基本的にはこのような手順を踏むことで、Editor上で表示されている情報は取得できるのではないのかと思います。
みなさんも困ったら是非試してみてください。

そして、まだまだ弊社のエンジニアからの発信は続きます。
明日は、gamuさんからの投稿になります。

また、Grengeへ興味を持って頂けた方は、是非弊社のサイトもご覧ください。
愉快なグレンジのメンバーが紹介されている珍しいサイトになっています。

それでは、最後まで読んで頂き、ありがとうございました。

コード全文は折りたたみ内に記載しました

コード全文

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEditor;

public static class MemoryModuleExporter
{
    private static readonly Type MemoryElementType = typeof(EditorWindow).Assembly.GetType("UnityEditor.MemoryElement");

    /// <summary>
    /// 標準ProfilerのMemoryで取得したデータをエクスポートする
    /// </summary>
    [MenuItem("Grenge/ExportMemorySample")]
    private static void ExportSampledData()
    {
        var memoryProfilerModule = GetMemoryProfilerModule();
        if (memoryProfilerModule == null) {
            return;
        }

        var referenceListViewFieldInfo = memoryProfilerModule.GetType().GetField("m_MemoryListView", BindingFlags.NonPublic | BindingFlags.Instance);
        var referenceListView = referenceListViewFieldInfo.GetValue(memoryProfilerModule);

        var rootMemoryElementField = referenceListView.GetType().GetField("m_Root", BindingFlags.NonPublic | BindingFlags.Instance);
        var rootMemoryElement = rootMemoryElementField.GetValue(referenceListView);
        if (rootMemoryElement == null) {
            Debug.LogWarning("You should take sample memory");
            return;
        }

        var rootElement = new MemoryElement(rootMemoryElement);
        rootElement.Name = "Root";
        ExportFile(rootElement);
    }

    /// <summary>
    /// ファイルに書き出す
    /// </summary>
    private static void ExportFile(MemoryElement rootElement)
    {
        var elementWriteInfos = new List<string>(rootElement.TotalChildCount);
        CreateElementWriteInfoList(elementWriteInfos, rootElement, "");

        // とりあえずファイル書き出し
        var exportFilePath = Application.persistentDataPath + Path.DirectorySeparatorChar + "memory.csv";
        using (StreamWriter writer = new StreamWriter(exportFilePath, false, Encoding.UTF8)) {
            foreach (var info in elementWriteInfos) {
                writer.WriteLine(info);
            }
        }

        Debug.Log("Exported: " + exportFilePath);
    }

    /// <summary>
    /// ファイル書き出し用のメモリ情報を作成する
    /// </summary>
    private static void CreateElementWriteInfoList(List<string> elementInfo, MemoryElement element, string parent)
    {
        var name = string.IsNullOrEmpty(parent) ? element.Name : parent + Path.DirectorySeparatorChar + element.Name;
        var info = $"\"{name}\",\"{element.GetMemorySize()}\"";
        elementInfo.Add(info);

        foreach (var child in element.Children) {
            CreateElementWriteInfoList(elementInfo, child, name);
        }
    }


    /// <summary>
    /// MemoryProfilerModuleを取得する
    /// </summary>
    private static object GetMemoryProfilerModule()
    {
        var profilerWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.ProfilerWindow");
        var profilerWindow = EditorWindow.GetWindow(profilerWindowType);
        var moduleFieldName =
#if UNITY_2021_2_OR_NEWER
            "m_AllModules";
#elif UNITY_2020_2_OR_NEWER
            "m_Modules";
#else
            "m_ProfilerModules";
#endif
        var moduleField = profilerWindow.GetType().GetField(moduleFieldName, BindingFlags.NonPublic | BindingFlags.Instance);
        if (moduleField == null) {
            // Unity2018はサポート外
            Debug.LogWarning("Not Supported Version.");
            return null;
        }

        var memoryProfilerModuleType = typeof(EditorWindow).Assembly.GetType("UnityEditorInternal.Profiling.MemoryProfilerModule");
        var moduleList = (IList)moduleField.GetValue(profilerWindow);
        foreach (var module in moduleList) {
            if (module.GetType() == memoryProfilerModuleType) {
                return module;
            }
        }

        Debug.LogWarning("Not find Memory Profiler Module");
        return null;
    }

    /// <summary>
    /// MemoryElement
    /// </summary>
    private class MemoryElement
    {
        private static readonly FieldInfo NameFiled = MemoryElementType.GetField("name", BindingFlags.Instance | BindingFlags.Public);
        private static readonly FieldInfo TotalMemoryField = MemoryElementType.GetField("totalMemory", BindingFlags.Instance | BindingFlags.Public);
        private static readonly FieldInfo TotalChildCountField = MemoryElementType.GetField("totalChildCount", BindingFlags.Instance | BindingFlags.Public);
        private static readonly FieldInfo DescriptionFiled = MemoryElementType.GetField("description", BindingFlags.Instance | BindingFlags.Public);
        private static readonly FieldInfo MemoryElementChildrenFiled = MemoryElementType.GetField("children", BindingFlags.Instance | BindingFlags.Public);

        private const double KiloByte = 1024;
        private const double MegaByte = KiloByte * 1024;
        private const double GigaByte = MegaByte * 1024;

        /// 名前
        public string Name;
        /// メモリ使用量の総量
        public long TotalMemory;
        /// 子供の数
        public int TotalChildCount;
        /// 説明
        public string Description;
        /// 子供のエレメント
        public List<MemoryElement> Children;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MemoryElement(object internalMemoryElement)
        {
            Name = (string)NameFiled.GetValue(internalMemoryElement);
            TotalMemory = (long)TotalMemoryField.GetValue(internalMemoryElement);
            TotalChildCount = (int)TotalChildCountField.GetValue(internalMemoryElement);
            Description = (string)DescriptionFiled.GetValue(internalMemoryElement);
            Children = new List<MemoryElement>(TotalChildCount);
            
            var children = (IList)MemoryElementChildrenFiled.GetValue(internalMemoryElement);
            if (children != null && children.Count != 0) {
                foreach (var child in children) {
                    var element = new MemoryElement(child);
                    Children.Add(element);
                }
            }
        }

        /// <summary>
        /// メモリサイズを取得する
        /// </summary>
        public string GetMemorySize()
        {
            var unit = "";
            double memory = 0;
            if (KiloByte > TotalMemory) {
                unit = "B";
                memory = TotalMemory;
            }
            else if (MegaByte > TotalMemory) {
                unit = "KB";
                memory = TotalMemory / KiloByte;
            }
            else if (GigaByte > TotalMemory) {
                unit = "MB";
                memory = TotalMemory / MegaByte;
            }
            else {
                unit = "GB";
                memory = TotalMemory / GigaByte;
            }

            var memorySize = (memory == 0) ? "0" : memory.ToString(".0");

            return $"{memorySize} {unit}";
        }
    }
}

出力したCSVは次のようになります。
output.png

10
1
1

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
10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?