using System; using System.Collections.Generic; using System.IO; using System.Text; using Unity.Profiling; using UnityEditor; using UnityEditor.Profiling; using UnityEditorInternal; using UnityEngine; using UnityEngine.Profiling; /// /// Profiler .data 文件导出器 /// 使用 ProfilerDriver.GetHierarchyFrameDataView API 提取函数级 GC Alloc / CPU 耗时 / 调用次数 /// 菜单: Tools > Profiler Data Exporter /// public class ProfilerDataExporter : EditorWindow { [MenuItem("Tools/Profiler Data Exporter")] public static void ShowWindow() { GetWindow("Profiler Exporter"); } private string dataFilePath = ""; private string outputDir = ""; private bool includeAllThreads = false; void OnEnable() { if (string.IsNullOrEmpty(outputDir)) outputDir = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); } void OnGUI() { GUILayout.Label("Profiler Data Exporter", EditorStyles.boldLabel); GUILayout.Label("用 ProfilerDriver API 提取函数级 GC/CPU 数据", EditorStyles.miniLabel); GUILayout.Space(10); // 数据文件选择 GUILayout.Label("1. 选择 .data 文件:"); GUILayout.BeginHorizontal(); dataFilePath = EditorGUILayout.TextField(dataFilePath); if (GUILayout.Button("浏览", GUILayout.Width(60))) { string picked = EditorUtility.OpenFilePanel("选择 Profiler .data 文件", "", "data"); if (!string.IsNullOrEmpty(picked)) dataFilePath = picked; } GUILayout.EndHorizontal(); GUILayout.Space(5); // 输出目录 GUILayout.Label("2. 输出目录:"); GUILayout.BeginHorizontal(); outputDir = EditorGUILayout.TextField(outputDir); if (GUILayout.Button("浏览", GUILayout.Width(60))) { string picked = EditorUtility.OpenFolderPanel("选择输出目录", outputDir, ""); if (!string.IsNullOrEmpty(picked)) outputDir = picked; } GUILayout.EndHorizontal(); GUILayout.Space(5); includeAllThreads = EditorGUILayout.Toggle("包含所有线程(否则仅主线程)", includeAllThreads); GUILayout.Space(15); if (GUILayout.Button("导出函数级数据 (GC + CPU)", GUILayout.Height(35))) { ExportFunctionLevelData(); } GUILayout.Space(5); if (GUILayout.Button("导出逐帧摘要 (帧时间 + GC)", GUILayout.Height(35))) { ExportPerFrameSummary(); } GUILayout.Space(5); if (GUILayout.Button("一键全量导出 (两个CSV)", GUILayout.Height(35))) { ExportFunctionLevelData(); ExportPerFrameSummary(); } } // ================================================================ // 加载 .data 并等待帧就绪(LoadProfile 是异步的,首次调用 firstFrameIndex 可能为 -1) // 返回 false 表示加载失败 // ================================================================ bool LoadAndValidate(out int firstFrame, out int lastFrame) { firstFrame = -1; lastFrame = -1; ProfilerDriver.LoadProfile(dataFilePath, false); // LoadProfile 异步,轮询等待帧索引就绪(最多 10 秒) DateTime deadline = DateTime.Now.AddSeconds(10); int retry = 0; while (DateTime.Now < deadline) { firstFrame = ProfilerDriver.firstFrameIndex; lastFrame = ProfilerDriver.lastFrameIndex; if (firstFrame >= 0 && lastFrame >= 0 && lastFrame >= firstFrame) return true; retry++; if (EditorUtility.DisplayCancelableProgressBar("加载 Profiler 数据", $"等待帧数据就绪... ({retry})", 0f)) { EditorUtility.ClearProgressBar(); return false; } // 让编辑器处理一帧,使 LoadProfile 继续推进 System.Threading.Thread.Sleep(50); } EditorUtility.ClearProgressBar(); Debug.LogError($"[ProfilerExport] 加载超时 firstFrame={firstFrame} lastFrame={lastFrame}"); return false; } // ================================================================ // 函数级导出:每个函数的 GC Alloc / Self Time / Total Time / Calls // ================================================================ void ExportFunctionLevelData() { if (!ValidateFile()) return; string baseName = Path.GetFileNameWithoutExtension(dataFilePath); string csvPath = Path.Combine(outputDir, baseName + "_functions.csv"); if (!LoadAndValidate(out int firstFrame, out int lastFrame)) { EditorUtility.DisplayDialog("错误", "数据文件加载失败或没有帧数据", "OK"); return; } // 聚合:函数名 -> 统计 var stats = new Dictionary(); int totalFrames = lastFrame - firstFrame + 1; int processed = 0; int threadCount = includeAllThreads ? 16 : 1; // 最多扫16个线程 for (int frame = firstFrame; frame <= lastFrame; frame++) { if (EditorUtility.DisplayCancelableProgressBar("导出函数级数据", $"帧 {frame} ({processed}/{totalFrames})", (float)processed / totalFrames)) break; for (int thread = 0; thread < threadCount; thread++) { using (var frameData = ProfilerDriver.GetHierarchyFrameDataView( frame, thread, HierarchyFrameDataView.ViewModes.MergeSamplesWithTheSameName, HierarchyFrameDataView.columnGcMemory, false)) { if (!frameData.valid) break; // 线程不存在,跳出 int rootId = frameData.GetRootItemID(); ProcessItemRecursive(frameData, rootId, stats); } } processed++; } EditorUtility.ClearProgressBar(); // 按 GC Alloc 降序排列 var sorted = new List(stats.Values); sorted.Sort((a, b) => b.totalGcAlloc.CompareTo(a.totalGcAlloc)); using (var writer = new StreamWriter(csvPath, false, Encoding.UTF8)) { writer.WriteLine("FunctionName,TotalGcAllocBytes,AvgGcPerCall,TotalCalls,SelfTimeMs,TotalTimeMs,FrameCount"); foreach (var s in sorted) { long avgGc = s.totalCalls > 0 ? s.totalGcAlloc / s.totalCalls : 0; writer.WriteLine($"{EscapeCsv(s.name)},{s.totalGcAlloc},{avgGc},{s.totalCalls},{s.selfTimeMs:F4},{s.totalTimeMs:F4},{s.frameCount}"); } } // 打印 Top 20 GC 分配函数 Debug.Log($"[ProfilerExport] 函数级数据已导出: {csvPath}"); Debug.Log($"[ProfilerExport] 共 {sorted.Count} 个函数,处理 {processed}/{totalFrames} 帧"); Debug.Log("[ProfilerExport] ====== GC Alloc Top 20 ======"); int topN = Math.Min(20, sorted.Count); for (int i = 0; i < topN; i++) { var s = sorted[i]; Debug.Log($" {i + 1}. {s.name} | GC={s.totalGcAlloc:N0}B | Calls={s.totalCalls} | Self={s.selfTimeMs:F2}ms"); } EditorUtility.DisplayDialog("完成", $"导出 {sorted.Count} 个函数到:\n{csvPath}\n\nTop 20 GC 函数已打印到 Console", "OK"); } // ================================================================ // 逐帧摘要:每帧的总时间、GC、DrawCall 等 // ================================================================ void ExportPerFrameSummary() { if (!ValidateFile()) return; string baseName = Path.GetFileNameWithoutExtension(dataFilePath); string csvPath = Path.Combine(outputDir, baseName + "_frames.csv"); if (!LoadAndValidate(out int firstFrame, out int lastFrame)) { EditorUtility.DisplayDialog("错误", "数据文件加载失败或没有帧数据", "OK"); return; } int totalFrames = lastFrame - firstFrame + 1; int processed = 0; using (var writer = new StreamWriter(csvPath, false, Encoding.UTF8)) { writer.WriteLine("FrameIndex,FrameTimeMs,Fps,TotalGcAllocBytes,DrawCalls,Batches,SetPassCalls,Triangles,Vertices"); for (int frame = firstFrame; frame <= lastFrame; frame++) { if (EditorUtility.DisplayCancelableProgressBar("导出逐帧摘要", $"帧 {frame} ({processed}/{totalFrames})", (float)processed / totalFrames)) break; float frameTimeMs = 0f; long frameGc = 0; int drawCalls = 0, batches = 0, setPass = 0, triangles = 0, vertices = 0; using (var frameData = ProfilerDriver.GetHierarchyFrameDataView( frame, 0, HierarchyFrameDataView.ViewModes.MergeSamplesWithTheSameName, HierarchyFrameDataView.columnGcMemory, false)) { if (frameData.valid) { frameTimeMs = frameData.frameTimeMs; // 遍历根节点找 GC Alloc 总量和渲染统计 int rootId = frameData.GetRootItemID(); var children = new List(); frameData.GetItemChildren(rootId, children); foreach (int childId in children) { string name = frameData.GetItemName(childId); float gc = frameData.GetItemColumnDataAsFloat(childId, HierarchyFrameDataView.columnGcMemory); frameGc += (long)gc; // 渲染相关 if (name == "Render") { var renderChildren = new List(); frameData.GetItemChildren(childId, renderChildren); foreach (int rid in renderChildren) { string rname = frameData.GetItemName(rid); float val = frameData.GetItemColumnDataAsFloat(rid, HierarchyFrameDataView.columnTotalTime); // 渲染统计需要用 ProfilerRecorder 获取 } } } } } // 用 ProfilerRecorder 获取渲染数据 using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Draw Calls Count", 1)) drawCalls = (int)rec.LastValue; using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Batches Count", 1)) batches = (int)rec.LastValue; using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "SetPass Calls Count", 1)) setPass = (int)rec.LastValue; using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Triangles Count", 1)) triangles = (int)rec.LastValue; using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Vertices Count", 1)) vertices = (int)rec.LastValue; float fps = frameTimeMs > 0 ? 1000f / frameTimeMs : 0; writer.WriteLine($"{frame},{frameTimeMs:F2},{fps:F1},{frameGc},{drawCalls},{batches},{setPass},{triangles},{vertices}"); processed++; } } EditorUtility.ClearProgressBar(); Debug.Log($"[ProfilerExport] 逐帧摘要已导出: {csvPath}"); EditorUtility.DisplayDialog("完成", $"逐帧摘要已导出到:\n{csvPath}", "OK"); } // ================================================================ // 递归遍历 hierarchy,累加每个函数的统计 // 每层递归用独立 List,避免 GetItemChildren 修改正在枚举的集合 // ================================================================ void ProcessItemRecursive(HierarchyFrameDataView frameData, int itemId, Dictionary stats) { ProcessItem(frameData, itemId, stats); // 递归子节点:每层新建 list,避免递归间共享集合被修改导致枚举异常 var children = new List(); frameData.GetItemChildren(itemId, children); foreach (int childId in children) { ProcessItemRecursive(frameData, childId, stats); } } void ProcessItem(HierarchyFrameDataView frameData, int itemId, Dictionary stats) { string name = frameData.GetItemName(itemId); if (string.IsNullOrEmpty(name)) return; float totalTime = frameData.GetItemColumnDataAsFloat(itemId, HierarchyFrameDataView.columnTotalTime); float selfTime = frameData.GetItemColumnDataAsFloat(itemId, HierarchyFrameDataView.columnSelfTime); float gcAlloc = frameData.GetItemColumnDataAsFloat(itemId, HierarchyFrameDataView.columnGcMemory); int calls = itemId >= 0 ? 1 : 0; // calls 列可能不直接可用,用出现次数代替 // 尝试获取 calls 列(如果该列存在) try { // 某些 Unity 版本支持 GetItemColumnDataAsInt var method = typeof(HierarchyFrameDataView).GetMethod("GetItemColumnDataAsInt", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); if (method != null) { calls = (int)method.Invoke(frameData, new object[] { itemId, HierarchyFrameDataView.columnCalls }); } } catch { /* 忽略,用默认值 */ } if (!stats.TryGetValue(name, out var s)) { s = new FuncStats { name = name }; stats[name] = s; } s.totalTimeMs += totalTime; s.selfTimeMs += selfTime; s.totalGcAlloc += (long)gcAlloc; s.totalCalls += calls; s.frameCount++; } // ================================================================ // 工具方法 // ================================================================ bool ValidateFile() { if (string.IsNullOrEmpty(dataFilePath) || !File.Exists(dataFilePath)) { EditorUtility.DisplayDialog("错误", "请选择有效的 .data 文件", "OK"); return false; } if (string.IsNullOrEmpty(outputDir) || !Directory.Exists(outputDir)) { EditorUtility.DisplayDialog("错误", "请选择有效的输出目录", "OK"); return false; } return true; } string EscapeCsv(string field) { if (field.Contains(",") || field.Contains("\"") || field.Contains("\n")) return "\"" + field.Replace("\"", "\"\"") + "\""; return field; } class FuncStats { public string name; public float totalTimeMs; public float selfTimeMs; public long totalGcAlloc; public int totalCalls; public int frameCount; } }