メモリ状態比較用エディタ拡張
環境
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; } }
感想
時間をかけずに適当に作りましたので、おかしい所や雑な所があると思います。