メモリ状態比較用エディタ拡張
環境
Unity2022.2.1f1
概要
プログラマ以外がMemoryProfilerを使うのも難しい事だろうと思い、簡単に比較できるように作ってみました。
コードは下の方にあります。
手順
- 適当なシーンを開く
今回の例ではNewSceneにCubeを1つ追加したものを使用しました。

- メニューのTools>EasyMemoryProfileDifferを押下し、ウィンドウを開く
小さい場合は広げてください。

- ウィンドウの左上の「TakeSample and ExportFile」ボタンを押下
メモリの状態を知りたい時にボタンを押します。
Unityのプロジェクトのフォルダに「MemoryLogs」というフォルダができ、中にその時点でのcsvが出来ます。
- 再度、ウィンドウの左上の「TakeSample and ExportFile」ボタンを押下
比較には2つのメモリ状態が必要なので、再度メモリ状態を知りたい時にボタンを押します。
今回の例では最初のシーンにさらにCubeを1つ追加した状態で押しました。

- ウィンドウの「LogA Path」と「LogB Path」に保存されているcsvを読みこむ
「...」ボタンを押してファイルを選択します。
2つとも読み込むと「DiffB - A(Add)」に追加されたもの「DiffA - B(Sub)」に削除されたものが表示されます。
今回の例ではCubeが1つ追加されたので「DiffB - A(Add)」の「Root/Scene Memory/GameObject/Cube(1)」やその他関連データが増えています。

コード
EasyMemoryProfileDifferWindow.cs
using UnityEngine;
using UnityEditor;
using System.IO;
public class EasyMemoryProfileDifferWindow : EditorWindow
{
[MenuItem("Tools/EasyMemoryProfileDiffer")]
public static void Create()
{
var window = GetWindow<EasyMemoryProfileDifferWindow>("EasyMemoryProfileDiffer");
window.Show();
}
private EasyMemoryProfileDiffer _obj = null;
private void OnEnable()
{
var ms = MonoScript.FromScriptableObject(this);
var path = AssetDatabase.GetAssetPath(ms);
if(string.IsNullOrEmpty(path) == false)
{
path = path.Replace(Path.GetFileName(path), "");
path = path + "EasyMemoryProfileDiffer.asset";
}
_obj = AssetDatabase.LoadAssetAtPath<EasyMemoryProfileDiffer>(path);
if(_obj == null)
{
var assets = AssetDatabase.LoadAllAssetsAtPath(path);
if(assets.Length > 0)
_obj = assets[0] as EasyMemoryProfileDiffer;
}
if(_obj == null)
{
_obj = ScriptableObject.CreateInstance<EasyMemoryProfileDiffer>();
AssetDatabase.CreateAsset(_obj, path);
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ImportRecursive);
}
_obj.Setup();
}
private void OnDisable()
{
if(_obj == null)
return;
EditorUtility.SetDirty(_obj);
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_obj), ImportAssetOptions.ForceUpdate | ImportAssetOptions.ImportRecursive);
}
private void OnGUI()
{
_obj.OnGUI();
Repaint();
}
}
EasyMemoryProfileDiffer.cs
using UnityEngine;
using UnityEditor.IMGUI.Controls;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
using System.Collections;
using System.Reflection;
using System;
using System.Text;
class EasyMemoryProfileDiffer : ScriptableObject
{
[System.Serializable]
public class TreeParam
{
public string path = null;
public TreeViewState treeViewState = null;
public Dictionary<MemoryParam, IDictionary> treeRoot = null;
public MemoryTreeView treeView = null;
public Rect treeViewRect;
public Vector2 scrollPos = Vector2.zero;
}
public TreeParam[] _treeParams = new TreeParam[4];
public void Setup()
{
for(int i = 0; i < _treeParams.Length; i++)
_treeParams[i].treeView = new MemoryTreeView(_treeParams[i].treeViewState);
for(int i = 0; i < 2; i++)
{
if(string.IsNullOrEmpty(_treeParams[i].path) == false)
LoadLogCsvFile(_treeParams[i]);
}
}
public void OnGUI()
{
using(new GUILayout.HorizontalScope())
{
if(GUILayout.Button("TakeSample and ExportFile", GUILayout.Width(200)) == true)
{
Export(true);
}
if(GUILayout.Button("ExportFile", GUILayout.Width(200)) == true)
{
Export(false);
}
if(GUILayout.Button("Profiler", GUILayout.Width(200)) == true)
{
GetProfilerWindow();
}
}
var width = 300.0f;
using(new GUILayout.HorizontalScope())
{
for(int i = 0; i < 2; i++)
{
using(var scrollView = new GUILayout.ScrollViewScope(_treeParams[i].scrollPos))
{
_treeParams[i].scrollPos = scrollView .scrollPosition;
using(new GUILayout.VerticalScope())
{
GUILayout.Label((i == 0) ? $"LogA Path" : $"LogB Path", GUILayout.Width(width));
Rect pathRect;
using(new GUILayout.HorizontalScope())
{
var prevPath = _treeParams[i].path;
_treeParams[i].path = EditorGUILayout.TextField("", _treeParams[i].path, GUILayout.Width(width - 50));
pathRect = GUILayoutUtility.GetLastRect();
if(GUILayout.Button("...", GUILayout.Width(50)) == true)
_treeParams[i].path = EditorUtility.OpenFilePanel("Select Log CsvFile", GetMemoryLogFolder(), "csv");
if(_treeParams[i].path != prevPath)
LoadLogCsvFile(_treeParams[i]);
}
if(Event.current.type == EventType.Repaint && _treeParams[i].treeView != null)
_treeParams[i].treeViewRect = new Rect(pathRect.x, pathRect.y + pathRect.height, width, _treeParams[i].treeView.totalHeight);
if(_treeParams[i].treeView != null)
_treeParams[i].treeView.OnGUI(_treeParams[i].treeViewRect);
var area = GUILayoutUtility.GetRect(new GUIContent( string.Empty ), GUIStyle.none, GUILayout.Width(_treeParams[i].treeViewRect.width), GUILayout.Height(_treeParams[i].treeViewRect.height));
}
}
}
for(int i = 0; i < 2; i++)
{
using(var scrollView = new GUILayout.ScrollViewScope(_treeParams[2 + i].scrollPos))
{
_treeParams[2 + i].scrollPos = scrollView .scrollPosition;
using(new GUILayout.VerticalScope())
{
GUILayout.Label((i == 0) ? $"Diff B - A (Add)" : $"Diff A - B (Sub)", GUILayout.Width(width));
var lastRect = GUILayoutUtility.GetLastRect();
if(_treeParams[2 + i].treeRoot != null)
{
if(Event.current.type == EventType.Repaint && _treeParams[2 + i].treeView != null)
_treeParams[2 + i].treeViewRect = new Rect(lastRect.x, lastRect.y + lastRect.height, width, _treeParams[2 + i].treeView.totalHeight);
if(_treeParams[2 + i].treeView != null)
_treeParams[2 + i].treeView.OnGUI(_treeParams[2 + i].treeViewRect);
var area = GUILayoutUtility.GetRect(new GUIContent( string.Empty ), GUIStyle.none, GUILayout.Width(_treeParams[2 + i].treeViewRect.width), GUILayout.Height(_treeParams[2 + i].treeViewRect.height));
}
}
}
}
}
}
public class MemoryParam
{
public string name = null;
public string value = null;
public bool Equals(MemoryParam other)
{
if(ReferenceEquals(null, other)) return false;
if(ReferenceEquals(this, other)) return true;
return other.name == name;
}
public override bool Equals(object obj)
{
if(ReferenceEquals(null, obj)) return false;
if(ReferenceEquals(this, obj)) return true;
if(obj.GetType() != typeof (MemoryParam)) return false;
return Equals((MemoryParam) obj);
}
public override int GetHashCode()
{
unchecked
{
return name.GetHashCode();
}
}
}
private void LoadLogCsvFile(TreeParam logParam)
{
var root = new Dictionary<MemoryParam, IDictionary>();
string csv = null;
if(File.Exists(logParam.path) == false)
{
Debug.LogWarning($"File Not Exists:{logParam.path}");
return;
}
using(var fs = File.Open(logParam.path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using(TextReader reader = new StreamReader(fs, Encoding.UTF8))
{
csv = reader.ReadToEnd();
}
}
if(csv == null)
return;
var lines = csv.Split(new string[]{"\r\n"}, System.StringSplitOptions.RemoveEmptyEntries);
var itemCount = lines.Length;
var nameCountDict = new Dictionary<string, int>();
for(int i = 0; i < itemCount; i++)
{
var line = lines[i];
var cells = line.Split(new string[]{"\""}, System.StringSplitOptions.RemoveEmptyEntries);
var name = cells[0];
var value = cells[2];
name = name.Replace("\"", "");
if(nameCountDict.TryGetValue(name, out int nameCount) == false)
{
nameCount = 0;
nameCountDict.Add(name, nameCount);
}
var nameIndex = nameCount;
nameCount++;
nameCountDict[name] = nameCount;
if(nameIndex > 0)
name = $"{name}?{nameIndex}";
var names = name.Split(new string[]{"\\"}, System.StringSplitOptions.RemoveEmptyEntries);
var dict = root;
MemoryParam key = null;
for(int n = 0; n < names.Length; n++)
{
key = new MemoryParam(){name = names[n]};
dict.TryGetValue(key, out IDictionary childDict);
if(childDict == null)
{
childDict = new Dictionary<MemoryParam, IDictionary>();
dict.Add(key, childDict);
}
dict = childDict as Dictionary<MemoryParam, IDictionary>;
}
key.value = value;
}
logParam.treeRoot = root;
logParam.treeView.Setup(root, true);
if(_treeParams[0].treeRoot != null && _treeParams[1].treeRoot != null)
CreateDiffTreeView();
}
private void CreateDiffTreeView()
{
//Add
var root = new Dictionary<MemoryParam, IDictionary>();
CopyTreeNodes(root, _treeParams[1].treeRoot);
RemoveTreeNodes(root, _treeParams[0].treeRoot);
_treeParams[2].treeRoot = root;
_treeParams[2].treeView.Setup(root, false);
//Sub
root = new Dictionary<MemoryParam, IDictionary>();
CopyTreeNodes(root, _treeParams[0].treeRoot);
RemoveTreeNodes(root, _treeParams[1].treeRoot);
_treeParams[3].treeRoot = root;
_treeParams[3].treeView.Setup(root, false);
}
private void CopyTreeNodes(Dictionary<MemoryParam, IDictionary> dest, Dictionary<MemoryParam, IDictionary> src)
{
foreach(var srcPair in src)
{
var childDict = new Dictionary<MemoryParam, IDictionary>();
CopyTreeNodes(childDict, srcPair.Value as Dictionary<MemoryParam, IDictionary>);
dest.Add(srcPair.Key, childDict);
}
}
private void RemoveTreeNodes(Dictionary<MemoryParam, IDictionary> dest, Dictionary<MemoryParam, IDictionary> src)
{
foreach(var srcPair in src)
{
var srcChildDict = srcPair.Value;
if(dest.TryGetValue(srcPair.Key, out IDictionary destChildDict) == true)
{
if(destChildDict != null)
{
RemoveTreeNodes(destChildDict as Dictionary<MemoryParam, IDictionary>, srcChildDict as Dictionary<MemoryParam, IDictionary>);
}
if(destChildDict == null || destChildDict.Count == 0)
dest.Remove(srcPair.Key);
}
}
}
private string GetMemoryLogFolder()
{
return Path.GetFullPath(Path.Combine( Application.dataPath, "../MemoryLogs" ));
}
private static EditorWindow GetProfilerWindow()
{
var profilerWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.ProfilerWindow");
var profilerWindow = EditorWindow.GetWindow(profilerWindowType);
return profilerWindow;
}
private static object GetMemoryProfilerModule()
{
var profilerWindow = GetProfilerWindow();
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;
}
private static readonly Type _memoryElementType = typeof(EditorWindow).Assembly.GetType("UnityEditor.MemoryElement");
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 _kibiByte = 1024;
private const double _mebiByte = _kibiByte * 1024;
private const double _gibiByte = _mebiByte * 1024;
public string _name = null;
public long _totalMemory = 0;
public int _totalChildCount = 0;
public string _description = null;
public List<MemoryElement> _children = null;
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);
}
}
}
public string GetMemorySize()
{
var unit = "";
double memory = 0;
if(_kibiByte > _totalMemory)
{
unit = "B";
memory = _totalMemory;
}
else if(_mebiByte > _totalMemory)
{
unit = "KB";
memory = _totalMemory / _kibiByte;
}
else if(_gibiByte > _totalMemory)
{
unit = "MB";
memory = _totalMemory / _mebiByte;
}
else
{
unit = "GB";
memory = _totalMemory / _gibiByte;
}
var memorySize = (memory == 0) ? "0" : memory.ToString(".0");
return $"{memorySize} {unit}";
}
}
private static void Export(bool isTakeSample)
{
var memoryProfilerModule = GetMemoryProfilerModule();
//TakeSample
if(isTakeSample == true)
{
var refreshMemoryData = memoryProfilerModule.GetType().GetMethod("RefreshMemoryData", BindingFlags.NonPublic | BindingFlags.Instance);
refreshMemoryData.Invoke(memoryProfilerModule, null);
}
//MemoryElements
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)
{
var rootElement = new MemoryElement(rootMemoryElement);
rootElement._name = "Root";
ExportFile(rootElement);
}
else
Debug.LogWarning("Not TakeSample");
}
private static void ExportFile(MemoryElement rootElement)
{
var elementWriteInfos = new List<string>(rootElement._totalChildCount);
CreateElementWriteInfoList(elementWriteInfos, rootElement, "");
string projectFolder = Path.Combine( Application.dataPath, "../" );
var exportFilePath = $"{projectFolder}/MemoryLogs/memory_{DateTime.Now.ToString("yyyy-MM-ddHHmmss")}.csv";
if(Directory.Exists(Path.GetDirectoryName(exportFilePath)) == false)
Directory.CreateDirectory(Path.GetDirectoryName(exportFilePath));
using(StreamWriter writer = new StreamWriter(exportFilePath, false, Encoding.UTF8))
{
foreach (var info in elementWriteInfos)
writer.WriteLine(info);
}
Debug.Log("Export Memory Csv Log File: " + exportFilePath);
}
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);
}
}
MemoryTreeView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEditor.IMGUI.Controls;
class MemoryTreeView : TreeView
{
private List<TreeViewItem> _treeItems = new List<TreeViewItem>();
public MemoryTreeView(TreeViewState treeViewState) : base(treeViewState)
{
Reload();
}
public void Setup(Dictionary<EasyMemoryProfileDiffer.MemoryParam, IDictionary> root, bool isValue)
{
_treeItems.Clear();
CreateTree(root, isValue, 0);
Reload();
}
private void CreateTree(Dictionary<EasyMemoryProfileDiffer.MemoryParam, IDictionary> dict, bool isValue, int depth)
{
foreach(var pair in dict)
{
var key = pair.Key;
_treeItems.Add(new TreeViewItem {id = _treeItems.Count + 1, depth = depth, displayName = ((isValue == true) ? $"{key.name}_{key.value}" : key.name)});
var childDict = pair.Value as Dictionary<EasyMemoryProfileDiffer.MemoryParam, IDictionary>;
CreateTree(childDict, isValue, depth + 1);
}
}
protected override TreeViewItem BuildRoot()
{
var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"};
SetupParentsAndChildrenFromDepths(root, _treeItems);
return root;
}
}
感想
時間をかけずに適当に作りましたので、おかしい所や雑な所があると思います。