chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Profiler
|
||||
{
|
||||
[McpForUnityTool("manage_profiler", AutoRegister = false, Group = "profiling")]
|
||||
public static class ManageProfiler
|
||||
{
|
||||
public static async Task<object> HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
|
||||
var p = new ToolParams(@params);
|
||||
string action = p.Get("action")?.ToLowerInvariant();
|
||||
|
||||
if (string.IsNullOrEmpty(action))
|
||||
return new ErrorResponse("'action' parameter is required.");
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
// Session
|
||||
case "profiler_start":
|
||||
return SessionOps.Start(@params);
|
||||
case "profiler_stop":
|
||||
return SessionOps.Stop(@params);
|
||||
case "profiler_status":
|
||||
return SessionOps.Status(@params);
|
||||
case "profiler_set_areas":
|
||||
return SessionOps.SetAreas(@params);
|
||||
|
||||
// Counters
|
||||
case "get_frame_timing":
|
||||
return FrameTimingOps.GetFrameTiming(@params);
|
||||
case "get_counters":
|
||||
return await CounterOps.GetCountersAsync(@params);
|
||||
case "get_object_memory":
|
||||
return ObjectMemoryOps.GetObjectMemory(@params);
|
||||
|
||||
// Memory Snapshot
|
||||
case "memory_take_snapshot":
|
||||
return await MemorySnapshotOps.TakeSnapshotAsync(@params);
|
||||
case "memory_list_snapshots":
|
||||
return MemorySnapshotOps.ListSnapshots(@params);
|
||||
case "memory_compare_snapshots":
|
||||
return MemorySnapshotOps.CompareSnapshots(@params);
|
||||
|
||||
// Frame Debugger
|
||||
case "frame_debugger_enable":
|
||||
return FrameDebuggerOps.Enable(@params);
|
||||
case "frame_debugger_disable":
|
||||
return FrameDebuggerOps.Disable(@params);
|
||||
case "frame_debugger_get_events":
|
||||
return FrameDebuggerOps.GetEvents(@params);
|
||||
|
||||
// Utility
|
||||
case "ping":
|
||||
return new SuccessResponse("manage_profiler is available.", new
|
||||
{
|
||||
tool = "manage_profiler",
|
||||
group = "profiling"
|
||||
});
|
||||
|
||||
default:
|
||||
return new ErrorResponse(
|
||||
$"Unknown action: '{action}'. Valid actions: "
|
||||
+ "profiler_start, profiler_stop, profiler_status, profiler_set_areas, "
|
||||
+ "get_frame_timing, get_counters, get_object_memory, "
|
||||
+ "memory_take_snapshot, memory_list_snapshots, memory_compare_snapshots, "
|
||||
+ "frame_debugger_enable, frame_debugger_disable, frame_debugger_get_events, "
|
||||
+ "ping.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"[ManageProfiler] Action '{action}' failed: {ex}");
|
||||
return new ErrorResponse($"Error in action '{action}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 369d21351c8c6c744a5775783d4957c9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69357f71e3f052245b353af3c57b521c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Unity.Profiling;
|
||||
using Unity.Profiling.LowLevel.Unsafe;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Profiler
|
||||
{
|
||||
internal static class CounterOps
|
||||
{
|
||||
internal static async Task<object> GetCountersAsync(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
var categoryResult = p.GetRequired("category");
|
||||
if (!categoryResult.IsSuccess)
|
||||
return new ErrorResponse(categoryResult.ErrorMessage);
|
||||
|
||||
string categoryName = categoryResult.Value;
|
||||
var resolved = ResolveCategory(categoryName, out string categoryError);
|
||||
if (resolved == null)
|
||||
return new ErrorResponse(categoryError);
|
||||
ProfilerCategory category = resolved.Value;
|
||||
|
||||
// Get counter names: explicit list or discover all in category
|
||||
var counterNames = GetRequestedCounters(p, category);
|
||||
if (counterNames.Count == 0)
|
||||
return new SuccessResponse($"No counters found in category '{categoryName}'.", new
|
||||
{
|
||||
category = categoryName,
|
||||
counters = new Dictionary<string, object>()
|
||||
});
|
||||
|
||||
// Start recorders
|
||||
var recorders = new List<ProfilerRecorder>();
|
||||
foreach (string name in counterNames)
|
||||
{
|
||||
recorders.Add(ProfilerRecorder.StartNew(category, name));
|
||||
}
|
||||
|
||||
var data = new Dictionary<string, object>();
|
||||
try
|
||||
{
|
||||
// Wait 1 frame for recorders to accumulate data
|
||||
await WaitOneFrameAsync();
|
||||
|
||||
// Read values — use GetSample(0) for last completed frame data;
|
||||
// CurrentValue is always 0 for per-frame render counters.
|
||||
for (int i = 0; i < recorders.Count; i++)
|
||||
{
|
||||
var recorder = recorders[i];
|
||||
string name = counterNames[i];
|
||||
long value = 0;
|
||||
if (recorder.Valid && recorder.Count > 0)
|
||||
value = recorder.GetSample(0).Value;
|
||||
else if (recorder.Valid)
|
||||
value = recorder.CurrentValue;
|
||||
data[name] = value;
|
||||
data[name + "_valid"] = recorder.Valid;
|
||||
data[name + "_unit"] = recorder.Valid ? recorder.UnitType.ToString() : "Unknown";
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var recorder in recorders)
|
||||
recorder.Dispose();
|
||||
}
|
||||
|
||||
return new SuccessResponse($"Captured {counterNames.Count} counter(s) from '{categoryName}'.", new
|
||||
{
|
||||
category = categoryName,
|
||||
counters = data,
|
||||
});
|
||||
}
|
||||
|
||||
private static List<string> GetRequestedCounters(ToolParams p, ProfilerCategory category)
|
||||
{
|
||||
var explicitCounters = p.GetStringArray("counters");
|
||||
if (explicitCounters != null && explicitCounters.Length > 0)
|
||||
return explicitCounters.ToList();
|
||||
|
||||
var allHandles = new List<ProfilerRecorderHandle>();
|
||||
ProfilerRecorderHandle.GetAvailable(allHandles);
|
||||
return allHandles
|
||||
.Select(h => ProfilerRecorderHandle.GetDescription(h))
|
||||
.Where(d => string.Equals(d.Category.Name, category.Name, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(d => d.Name)
|
||||
.OrderBy(n => n)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static Task WaitOneFrameAsync()
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
int remaining = 2;
|
||||
|
||||
void Tick()
|
||||
{
|
||||
if (--remaining > 0) return;
|
||||
EditorApplication.update -= Tick;
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
EditorApplication.update += Tick;
|
||||
try { EditorApplication.QueuePlayerLoopUpdate(); } catch { /* throttled editor */ }
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static readonly string[] ValidCategories = new[]
|
||||
{
|
||||
"Render", "Scripts", "Memory", "Physics",
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
"Physics2D",
|
||||
#endif
|
||||
"Animation",
|
||||
"Audio", "Lighting", "Network", "Gui", "UI", "Ai", "Video",
|
||||
"Loading", "Input", "Vr", "Internal", "Particles", "FileIO", "VirtualTexturing"
|
||||
};
|
||||
|
||||
internal static ProfilerCategory? ResolveCategory(string name, out string error)
|
||||
{
|
||||
error = null;
|
||||
switch (name.ToLowerInvariant())
|
||||
{
|
||||
case "render": return ProfilerCategory.Render;
|
||||
case "scripts": return ProfilerCategory.Scripts;
|
||||
case "memory": return ProfilerCategory.Memory;
|
||||
case "physics": return ProfilerCategory.Physics;
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
case "physics2d": return ProfilerCategory.Physics2D;
|
||||
#endif
|
||||
case "animation": return ProfilerCategory.Animation;
|
||||
case "audio": return ProfilerCategory.Audio;
|
||||
case "lighting": return ProfilerCategory.Lighting;
|
||||
case "network": return ProfilerCategory.Network;
|
||||
case "gui": case "ui": return ProfilerCategory.Gui;
|
||||
case "ai": return ProfilerCategory.Ai;
|
||||
case "video": return ProfilerCategory.Video;
|
||||
case "loading": return ProfilerCategory.Loading;
|
||||
case "input": return ProfilerCategory.Input;
|
||||
case "vr": return ProfilerCategory.Vr;
|
||||
case "internal": return ProfilerCategory.Internal;
|
||||
case "particles": return ProfilerCategory.Particles;
|
||||
case "fileio": return ProfilerCategory.FileIO;
|
||||
case "virtualtexturing": return ProfilerCategory.VirtualTexturing;
|
||||
default:
|
||||
error = $"Unknown category '{name}'. Valid: {string.Join(", ", ValidCategories)}";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d082e7102c501e14084295fdd363f2e4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,260 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Profiler
|
||||
{
|
||||
internal static class FrameDebuggerOps
|
||||
{
|
||||
private static readonly Type UtilType;
|
||||
private static readonly PropertyInfo EventCountProp;
|
||||
private static readonly MethodInfo EnableMethod;
|
||||
private static readonly MethodInfo GetFrameEventsMethod;
|
||||
private static readonly MethodInfo GetEventDataMethod;
|
||||
private static readonly MethodInfo GetEventInfoNameMethod;
|
||||
private static readonly Type EventDataType;
|
||||
private static readonly bool Available;
|
||||
|
||||
static FrameDebuggerOps()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Unity 6+: moved to FrameDebuggerInternal sub-namespace
|
||||
UtilType = Type.GetType("UnityEditorInternal.FrameDebuggerInternal.FrameDebuggerUtility, UnityEditor");
|
||||
// Unity 2021–2022: original location
|
||||
UtilType ??= Type.GetType("UnityEditorInternal.FrameDebuggerUtility, UnityEditor");
|
||||
|
||||
if (UtilType == null) return;
|
||||
|
||||
EventCountProp = UtilType.GetProperty("count", BindingFlags.Public | BindingFlags.Static)
|
||||
?? UtilType.GetProperty("eventsCount", BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
EnableMethod = UtilType.GetMethod("SetEnabled", BindingFlags.Public | BindingFlags.Static,
|
||||
null, new[] { typeof(bool), typeof(int) }, null)
|
||||
?? UtilType.GetMethod("SetEnabled", BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
GetFrameEventsMethod = UtilType.GetMethod("GetFrameEvents", BindingFlags.Public | BindingFlags.Static);
|
||||
GetEventInfoNameMethod = UtilType.GetMethod("GetFrameEventInfoName", BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
// Unity 6: GetFrameEventData(int, FrameDebuggerEventData) — 2 params, returns bool
|
||||
// Older: GetFrameEventData(int) — 1 param, returns event data object
|
||||
EventDataType = Type.GetType("UnityEditorInternal.FrameDebuggerInternal.FrameDebuggerEventData, UnityEditor")
|
||||
?? Type.GetType("UnityEditorInternal.FrameDebuggerEventData, UnityEditor");
|
||||
|
||||
if (EventDataType != null)
|
||||
{
|
||||
GetEventDataMethod = UtilType.GetMethod("GetFrameEventData", BindingFlags.Public | BindingFlags.Static,
|
||||
null, new[] { typeof(int), EventDataType }, null);
|
||||
}
|
||||
GetEventDataMethod ??= UtilType.GetMethod("GetFrameEventData", BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
Available = EventCountProp != null && EnableMethod != null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Available = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static object Enable(JObject @params)
|
||||
{
|
||||
if (!Available)
|
||||
return new ErrorResponse("FrameDebuggerUtility not found via reflection.");
|
||||
|
||||
// Open the Frame Debugger window (required for event capture)
|
||||
EditorApplication.ExecuteMenuItem("Window/Analysis/Frame Debugger");
|
||||
|
||||
// Frame Debugger requires game to be paused before enabling to capture events.
|
||||
if (EditorApplication.isPlaying && !EditorApplication.isPaused)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
"Game must be paused before enabling Frame Debugger. "
|
||||
+ "Call manage_editor action=pause first, then retry frame_debugger_enable.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InvokeSetEnabled(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ErrorResponse($"Failed to enable Frame Debugger: {ex.Message}");
|
||||
}
|
||||
|
||||
int eventCount = GetEventCount();
|
||||
return new SuccessResponse("Frame Debugger enabled.", new
|
||||
{
|
||||
enabled = true,
|
||||
event_count = eventCount,
|
||||
});
|
||||
}
|
||||
|
||||
internal static object Disable(JObject @params)
|
||||
{
|
||||
if (!Available)
|
||||
return new ErrorResponse("FrameDebuggerUtility not found via reflection.");
|
||||
|
||||
try
|
||||
{
|
||||
InvokeSetEnabled(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ErrorResponse($"Failed to disable Frame Debugger: {ex.Message}");
|
||||
}
|
||||
|
||||
return new SuccessResponse("Frame Debugger disabled.", new { enabled = false });
|
||||
}
|
||||
|
||||
internal static object GetEvents(JObject @params)
|
||||
{
|
||||
if (!Available)
|
||||
return new ErrorResponse("FrameDebuggerUtility not found via reflection.");
|
||||
|
||||
var p = new ToolParams(@params);
|
||||
int pageSize = p.GetInt("page_size") ?? 50;
|
||||
int cursor = p.GetInt("cursor") ?? 0;
|
||||
|
||||
int totalEvents = GetEventCount();
|
||||
if (totalEvents == 0)
|
||||
{
|
||||
return new SuccessResponse("Frame Debugger has no events. Is it enabled?", new
|
||||
{
|
||||
events = new List<object>(),
|
||||
total_events = 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Try GetFrameEvents() for the event descriptor array (has type/name info)
|
||||
object[] frameEvents = null;
|
||||
if (GetFrameEventsMethod != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = GetFrameEventsMethod.Invoke(null, null);
|
||||
if (raw is Array arr)
|
||||
{
|
||||
frameEvents = new object[arr.Length];
|
||||
arr.CopyTo(frameEvents, 0);
|
||||
}
|
||||
}
|
||||
catch { /* fall through */ }
|
||||
}
|
||||
|
||||
var events = new List<object>();
|
||||
int end = Math.Min(cursor + pageSize, totalEvents);
|
||||
|
||||
for (int i = cursor; i < end; i++)
|
||||
{
|
||||
var entry = new Dictionary<string, object> { ["index"] = i };
|
||||
|
||||
// Get event name
|
||||
if (GetEventInfoNameMethod != null)
|
||||
{
|
||||
try { entry["name"] = (string)GetEventInfoNameMethod.Invoke(null, new object[] { i }); }
|
||||
catch { /* skip */ }
|
||||
}
|
||||
|
||||
// Get fields from FrameDebuggerEvent descriptor
|
||||
if (frameEvents != null && i < frameEvents.Length)
|
||||
{
|
||||
var desc = frameEvents[i];
|
||||
var descType = desc.GetType();
|
||||
TryAddField(descType, desc, "type", entry, "event_type");
|
||||
TryAddField(descType, desc, "gameObjectInstanceID", entry);
|
||||
}
|
||||
|
||||
// Get detailed event data
|
||||
if (GetEventDataMethod != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var paramInfos = GetEventDataMethod.GetParameters();
|
||||
object eventData;
|
||||
|
||||
if (paramInfos.Length == 2 && EventDataType != null)
|
||||
{
|
||||
// Unity 6: bool GetFrameEventData(int, FrameDebuggerEventData)
|
||||
eventData = Activator.CreateInstance(EventDataType);
|
||||
var args = new object[] { i, eventData };
|
||||
var ok = GetEventDataMethod.Invoke(null, args);
|
||||
eventData = (ok is true) ? args[1] : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Older: FrameDebuggerEventData GetFrameEventData(int)
|
||||
eventData = GetEventDataMethod.Invoke(null, new object[] { i });
|
||||
}
|
||||
|
||||
if (eventData != null)
|
||||
{
|
||||
var edType = eventData.GetType();
|
||||
TryAddField(edType, eventData, "shaderName", entry);
|
||||
TryAddField(edType, eventData, "passName", entry);
|
||||
TryAddField(edType, eventData, "rtName", entry);
|
||||
TryAddField(edType, eventData, "rtWidth", entry);
|
||||
TryAddField(edType, eventData, "rtHeight", entry);
|
||||
TryAddField(edType, eventData, "vertexCount", entry);
|
||||
TryAddField(edType, eventData, "indexCount", entry);
|
||||
TryAddField(edType, eventData, "instanceCount", entry);
|
||||
TryAddField(edType, eventData, "meshName", entry);
|
||||
}
|
||||
}
|
||||
catch { /* skip event data for this index */ }
|
||||
}
|
||||
|
||||
events.Add(entry);
|
||||
}
|
||||
|
||||
var result = new Dictionary<string, object>
|
||||
{
|
||||
["events"] = events,
|
||||
["total_events"] = totalEvents,
|
||||
["page_size"] = pageSize,
|
||||
["cursor"] = cursor,
|
||||
};
|
||||
if (end < totalEvents)
|
||||
result["next_cursor"] = end;
|
||||
|
||||
return new SuccessResponse($"Frame Debugger events {cursor}-{end - 1} of {totalEvents}.", result);
|
||||
}
|
||||
|
||||
private static void InvokeSetEnabled(bool value)
|
||||
{
|
||||
int paramCount = EnableMethod.GetParameters().Length;
|
||||
if (paramCount == 2)
|
||||
EnableMethod.Invoke(null, new object[] { value, 0 });
|
||||
else if (paramCount == 1)
|
||||
EnableMethod.Invoke(null, new object[] { value });
|
||||
else
|
||||
throw new InvalidOperationException($"SetEnabled has unexpected {paramCount} parameters.");
|
||||
}
|
||||
|
||||
private static int GetEventCount()
|
||||
{
|
||||
try { return (int)EventCountProp.GetValue(null); }
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
private static void TryAddField(Type type, object obj, string fieldName, Dictionary<string, object> dict, string outputKey = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance)
|
||||
?? type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var prop = type.GetProperty(fieldName, BindingFlags.Public | BindingFlags.Instance)
|
||||
?? type.GetProperty(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
object val = field != null ? field.GetValue(obj)
|
||||
: prop != null ? prop.GetValue(obj)
|
||||
: null;
|
||||
if (val != null)
|
||||
dict[outputKey ?? fieldName] = val.GetType().IsEnum ? val.ToString() : val;
|
||||
}
|
||||
catch { /* skip unavailable fields */ }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c99fe029dffcb945a39beda3acf304e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,56 @@
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Profiler
|
||||
{
|
||||
internal static class FrameTimingOps
|
||||
{
|
||||
internal static object GetFrameTiming(JObject @params)
|
||||
{
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
if (!FrameTimingManager.IsFeatureEnabled())
|
||||
{
|
||||
return new ErrorResponse(
|
||||
"Frame Timing Stats is not enabled. "
|
||||
+ "Enable it in Project Settings > Player > Other Settings > 'Frame Timing Stats', "
|
||||
+ "or use a Development Build (always enabled).");
|
||||
}
|
||||
#endif
|
||||
|
||||
FrameTimingManager.CaptureFrameTimings();
|
||||
var timings = new FrameTiming[1];
|
||||
uint count = FrameTimingManager.GetLatestTimings(1, timings);
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return new SuccessResponse("No frame timing data available yet (need a few frames).", new
|
||||
{
|
||||
available = false,
|
||||
});
|
||||
}
|
||||
|
||||
var t = timings[0];
|
||||
return new SuccessResponse("Frame timing captured.", new
|
||||
{
|
||||
available = true,
|
||||
cpu_frame_time_ms = t.cpuFrameTime,
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
cpu_main_thread_frame_time_ms = t.cpuMainThreadFrameTime,
|
||||
cpu_main_thread_present_wait_time_ms = t.cpuMainThreadPresentWaitTime,
|
||||
cpu_render_thread_frame_time_ms = t.cpuRenderThreadFrameTime,
|
||||
#endif
|
||||
gpu_frame_time_ms = t.gpuFrameTime,
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
frame_start_timestamp = t.frameStartTimestamp,
|
||||
first_submit_timestamp = t.firstSubmitTimestamp,
|
||||
#endif
|
||||
cpu_time_present_called = t.cpuTimePresentCalled,
|
||||
cpu_time_frame_complete = t.cpuTimeFrameComplete,
|
||||
height_scale = t.heightScale,
|
||||
width_scale = t.widthScale,
|
||||
sync_interval = t.syncInterval,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aace6e3da2222164298de24af50cab3b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Profiler
|
||||
{
|
||||
internal static class MemorySnapshotOps
|
||||
{
|
||||
private static readonly Type MemoryProfilerType =
|
||||
Type.GetType("Unity.Profiling.Memory.MemoryProfiler, UnityEngine.CoreModule")
|
||||
?? Type.GetType("UnityEngine.Profiling.Memory.Experimental.MemoryProfiler, UnityEngine.CoreModule")
|
||||
?? Type.GetType("Unity.MemoryProfiler.MemoryProfiler, Unity.MemoryProfiler.Editor");
|
||||
|
||||
private static bool HasPackage => MemoryProfilerType != null;
|
||||
|
||||
internal static async Task<object> TakeSnapshotAsync(JObject @params)
|
||||
{
|
||||
if (!HasPackage)
|
||||
return PackageMissingError();
|
||||
|
||||
var p = new ToolParams(@params);
|
||||
string snapshotPath = p.Get("snapshot_path");
|
||||
|
||||
if (string.IsNullOrEmpty(snapshotPath))
|
||||
{
|
||||
string dir = Path.Combine(Application.temporaryCachePath, "MemoryCaptures");
|
||||
Directory.CreateDirectory(dir);
|
||||
snapshotPath = Path.Combine(dir, $"snapshot_{DateTime.Now:yyyyMMdd_HHmmss}.snap");
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
try
|
||||
{
|
||||
var debugScreenCaptureType = Type.GetType(
|
||||
"Unity.Profiling.DebugScreenCapture, UnityEngine.CoreModule")
|
||||
?? Type.GetType("UnityEngine.Profiling.Experimental.DebugScreenCapture, UnityEngine.CoreModule")
|
||||
?? Type.GetType("Unity.Profiling.Memory.Experimental.DebugScreenCapture, Unity.MemoryProfiler.Editor");
|
||||
var captureFlagsType = Type.GetType(
|
||||
"Unity.Profiling.Memory.CaptureFlags, UnityEngine.CoreModule")
|
||||
?? Type.GetType("UnityEngine.Profiling.Memory.Experimental.CaptureFlags, UnityEngine.CoreModule");
|
||||
|
||||
System.Reflection.MethodInfo takeMethod = null;
|
||||
|
||||
if (debugScreenCaptureType != null && captureFlagsType != null)
|
||||
{
|
||||
var screenshotCallbackType = typeof(Action<,,>).MakeGenericType(
|
||||
typeof(string), typeof(bool), debugScreenCaptureType);
|
||||
takeMethod = MemoryProfilerType.GetMethod("TakeSnapshot",
|
||||
new[] { typeof(string), typeof(Action<string, bool>), screenshotCallbackType, captureFlagsType });
|
||||
}
|
||||
|
||||
if (takeMethod == null && captureFlagsType != null)
|
||||
{
|
||||
takeMethod = MemoryProfilerType.GetMethod("TakeSnapshot",
|
||||
new[] { typeof(string), typeof(Action<string, bool>), captureFlagsType });
|
||||
}
|
||||
|
||||
if (takeMethod == null && debugScreenCaptureType != null)
|
||||
{
|
||||
var screenshotCallbackType = typeof(Action<,,>).MakeGenericType(
|
||||
typeof(string), typeof(bool), debugScreenCaptureType);
|
||||
takeMethod = MemoryProfilerType.GetMethod("TakeSnapshot",
|
||||
new[] { typeof(string), typeof(Action<string, bool>), screenshotCallbackType, typeof(uint) });
|
||||
}
|
||||
|
||||
if (takeMethod == null)
|
||||
{
|
||||
takeMethod = MemoryProfilerType.GetMethod("TakeSnapshot",
|
||||
new[] { typeof(string), typeof(Action<string, bool>) });
|
||||
}
|
||||
|
||||
if (takeMethod == null)
|
||||
return new ErrorResponse("Could not find TakeSnapshot method on MemoryProfiler. API may have changed.");
|
||||
|
||||
Action<string, bool> callback = (path, result) =>
|
||||
{
|
||||
if (result)
|
||||
{
|
||||
var fi = new FileInfo(path);
|
||||
tcs.TrySetResult(new SuccessResponse("Memory snapshot captured.", new
|
||||
{
|
||||
path,
|
||||
size_bytes = fi.Exists ? fi.Length : 0,
|
||||
size_mb = fi.Exists ? Math.Round(fi.Length / (1024.0 * 1024.0), 2) : 0,
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.TrySetResult(new ErrorResponse($"Snapshot capture failed for path: {path}"));
|
||||
}
|
||||
};
|
||||
|
||||
var takeMethodParams = takeMethod.GetParameters();
|
||||
int paramCount = takeMethodParams.Length;
|
||||
if (paramCount == 4 && takeMethodParams[3].ParameterType == captureFlagsType)
|
||||
takeMethod.Invoke(null, new object[] { snapshotPath, callback, null, Enum.ToObject(captureFlagsType, 0) });
|
||||
else if (paramCount == 3 && takeMethodParams[2].ParameterType == captureFlagsType)
|
||||
takeMethod.Invoke(null, new object[] { snapshotPath, callback, Enum.ToObject(captureFlagsType, 0) });
|
||||
else if (paramCount == 4 && takeMethodParams[3].ParameterType == typeof(uint))
|
||||
takeMethod.Invoke(null, new object[] { snapshotPath, callback, null, 0u });
|
||||
else if (paramCount == 2)
|
||||
takeMethod.Invoke(null, new object[] { snapshotPath, callback });
|
||||
else
|
||||
return new ErrorResponse($"TakeSnapshot has unexpected {paramCount} parameters. API may have changed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ErrorResponse($"Failed to take snapshot: {ex.Message}");
|
||||
}
|
||||
|
||||
var timeout = Task.Delay(TimeSpan.FromSeconds(30));
|
||||
var completed = await Task.WhenAny(tcs.Task, timeout);
|
||||
if (completed == timeout)
|
||||
return new ErrorResponse("Snapshot timed out after 30 seconds.");
|
||||
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
internal static object ListSnapshots(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string searchPath = p.Get("search_path");
|
||||
|
||||
var dirs = new List<string>();
|
||||
if (!string.IsNullOrEmpty(searchPath))
|
||||
{
|
||||
dirs.Add(searchPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
dirs.Add(Path.Combine(Application.temporaryCachePath, "MemoryCaptures"));
|
||||
dirs.Add(Path.Combine(Application.dataPath, "..", "MemoryCaptures"));
|
||||
}
|
||||
|
||||
var snapshots = new List<object>();
|
||||
foreach (string dir in dirs)
|
||||
{
|
||||
if (!Directory.Exists(dir)) continue;
|
||||
foreach (string file in Directory.GetFiles(dir, "*.snap"))
|
||||
{
|
||||
var fi = new FileInfo(file);
|
||||
snapshots.Add(new
|
||||
{
|
||||
path = fi.FullName,
|
||||
size_bytes = fi.Length,
|
||||
size_mb = Math.Round(fi.Length / (1024.0 * 1024.0), 2),
|
||||
created = fi.CreationTimeUtc.ToString("o"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new SuccessResponse($"Found {snapshots.Count} snapshot(s).", new
|
||||
{
|
||||
snapshots,
|
||||
searched_dirs = dirs,
|
||||
});
|
||||
}
|
||||
|
||||
internal static object CompareSnapshots(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
var pathAResult = p.GetRequired("snapshot_a");
|
||||
if (!pathAResult.IsSuccess)
|
||||
return new ErrorResponse(pathAResult.ErrorMessage);
|
||||
|
||||
var pathBResult = p.GetRequired("snapshot_b");
|
||||
if (!pathBResult.IsSuccess)
|
||||
return new ErrorResponse(pathBResult.ErrorMessage);
|
||||
|
||||
string pathA = pathAResult.Value;
|
||||
string pathB = pathBResult.Value;
|
||||
|
||||
if (!File.Exists(pathA))
|
||||
return new ErrorResponse($"Snapshot file not found: {pathA}");
|
||||
if (!File.Exists(pathB))
|
||||
return new ErrorResponse($"Snapshot file not found: {pathB}");
|
||||
|
||||
var fiA = new FileInfo(pathA);
|
||||
var fiB = new FileInfo(pathB);
|
||||
|
||||
return new SuccessResponse("Snapshot comparison (file-level metadata).", new
|
||||
{
|
||||
snapshot_a = new
|
||||
{
|
||||
path = fiA.FullName,
|
||||
size_bytes = fiA.Length,
|
||||
size_mb = Math.Round(fiA.Length / (1024.0 * 1024.0), 2),
|
||||
created = fiA.CreationTimeUtc.ToString("o"),
|
||||
},
|
||||
snapshot_b = new
|
||||
{
|
||||
path = fiB.FullName,
|
||||
size_bytes = fiB.Length,
|
||||
size_mb = Math.Round(fiB.Length / (1024.0 * 1024.0), 2),
|
||||
created = fiB.CreationTimeUtc.ToString("o"),
|
||||
},
|
||||
delta = new
|
||||
{
|
||||
size_delta_bytes = fiB.Length - fiA.Length,
|
||||
size_delta_mb = Math.Round((fiB.Length - fiA.Length) / (1024.0 * 1024.0), 2),
|
||||
time_delta_seconds = (fiB.CreationTimeUtc - fiA.CreationTimeUtc).TotalSeconds,
|
||||
},
|
||||
note = "For detailed object-level comparison, open both snapshots in the Memory Profiler window.",
|
||||
});
|
||||
}
|
||||
|
||||
private static ErrorResponse PackageMissingError()
|
||||
{
|
||||
return new ErrorResponse(
|
||||
"Package com.unity.memoryprofiler is required. "
|
||||
+ "Install via Package Manager or: manage_packages action=add_package package_id=com.unity.memoryprofiler");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb63d9b6a47327b40883452637eeb075
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
using UProfiler = UnityEngine.Profiling.Profiler;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Profiler
|
||||
{
|
||||
internal static class ObjectMemoryOps
|
||||
{
|
||||
internal static object GetObjectMemory(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
var objectPathResult = p.GetRequired("object_path");
|
||||
if (!objectPathResult.IsSuccess)
|
||||
return new ErrorResponse(objectPathResult.ErrorMessage);
|
||||
|
||||
string objectPath = objectPathResult.Value;
|
||||
|
||||
// Try scene hierarchy first
|
||||
var go = GameObject.Find(objectPath);
|
||||
if (go != null)
|
||||
{
|
||||
long bytes = UProfiler.GetRuntimeMemorySizeLong(go);
|
||||
return new SuccessResponse($"Memory for '{objectPath}'.", new
|
||||
{
|
||||
object_name = go.name,
|
||||
object_type = go.GetType().Name,
|
||||
size_bytes = bytes,
|
||||
size_mb = Math.Round(bytes / (1024.0 * 1024.0), 3),
|
||||
source = "scene_hierarchy",
|
||||
});
|
||||
}
|
||||
|
||||
// Try asset path
|
||||
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(objectPath);
|
||||
if (asset != null)
|
||||
{
|
||||
long bytes = UProfiler.GetRuntimeMemorySizeLong(asset);
|
||||
return new SuccessResponse($"Memory for '{objectPath}'.", new
|
||||
{
|
||||
object_name = asset.name,
|
||||
object_type = asset.GetType().Name,
|
||||
size_bytes = bytes,
|
||||
size_mb = Math.Round(bytes / (1024.0 * 1024.0), 3),
|
||||
source = "asset_database",
|
||||
});
|
||||
}
|
||||
|
||||
return new ErrorResponse($"Object not found at path '{objectPath}'. Try a scene hierarchy path (e.g. /Player/Mesh) or an asset path (e.g. Assets/Textures/hero.png).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d57a224350157834083a7aabc86dc4e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
using UProfiler = UnityEngine.Profiling.Profiler;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Profiler
|
||||
{
|
||||
internal static class SessionOps
|
||||
{
|
||||
private static readonly string[] AreaNames = Enum.GetNames(typeof(ProfilerArea));
|
||||
|
||||
internal static object Start(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string logFile = p.Get("log_file");
|
||||
bool enableCallstacks = p.GetBool("enable_callstacks");
|
||||
|
||||
UProfiler.enabled = true;
|
||||
|
||||
if (!string.IsNullOrEmpty(logFile))
|
||||
{
|
||||
string dir = Path.GetDirectoryName(logFile);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
return new ErrorResponse($"Log file directory does not exist: {dir}");
|
||||
|
||||
UProfiler.logFile = logFile;
|
||||
UProfiler.enableBinaryLog = true;
|
||||
}
|
||||
|
||||
if (enableCallstacks)
|
||||
UProfiler.enableAllocationCallstacks = true;
|
||||
|
||||
return new SuccessResponse("Profiler started.", new
|
||||
{
|
||||
enabled = UProfiler.enabled,
|
||||
recording = UProfiler.enableBinaryLog,
|
||||
log_file = UProfiler.enableBinaryLog ? UProfiler.logFile : null,
|
||||
allocation_callstacks = UProfiler.enableAllocationCallstacks,
|
||||
});
|
||||
}
|
||||
|
||||
internal static object Stop(JObject @params)
|
||||
{
|
||||
string previousLogFile = UProfiler.enableBinaryLog ? UProfiler.logFile : null;
|
||||
|
||||
UProfiler.enableBinaryLog = false;
|
||||
UProfiler.enableAllocationCallstacks = false;
|
||||
UProfiler.enabled = false;
|
||||
|
||||
return new SuccessResponse("Profiler stopped.", new
|
||||
{
|
||||
enabled = false,
|
||||
previous_log_file = previousLogFile,
|
||||
});
|
||||
}
|
||||
|
||||
internal static object Status(JObject @params)
|
||||
{
|
||||
var areas = new Dictionary<string, bool>();
|
||||
foreach (string name in AreaNames)
|
||||
{
|
||||
if (Enum.TryParse<ProfilerArea>(name, out var area))
|
||||
areas[name] = UProfiler.GetAreaEnabled(area);
|
||||
}
|
||||
|
||||
return new SuccessResponse("Profiler status.", new
|
||||
{
|
||||
enabled = UProfiler.enabled,
|
||||
recording = UProfiler.enableBinaryLog,
|
||||
log_file = UProfiler.enableBinaryLog ? UProfiler.logFile : null,
|
||||
allocation_callstacks = UProfiler.enableAllocationCallstacks,
|
||||
areas,
|
||||
});
|
||||
}
|
||||
|
||||
internal static object SetAreas(JObject @params)
|
||||
{
|
||||
var areasToken = @params["areas"] as JObject;
|
||||
if (areasToken == null)
|
||||
return new ErrorResponse($"'areas' parameter required. Valid areas: {string.Join(", ", AreaNames)}");
|
||||
|
||||
var updated = new Dictionary<string, bool>();
|
||||
foreach (var prop in areasToken.Properties())
|
||||
{
|
||||
if (!Enum.TryParse<ProfilerArea>(prop.Name, true, out var area))
|
||||
return new ErrorResponse($"Unknown area '{prop.Name}'. Valid: {string.Join(", ", AreaNames)}");
|
||||
|
||||
if (prop.Value.Type != JTokenType.Boolean)
|
||||
return new ErrorResponse($"Area '{prop.Name}' value must be a boolean (true/false), got: {prop.Value}");
|
||||
bool enabled = prop.Value.ToObject<bool>();
|
||||
UProfiler.SetAreaEnabled(area, enabled);
|
||||
updated[prop.Name] = enabled;
|
||||
}
|
||||
|
||||
return new SuccessResponse($"Updated {updated.Count} profiler area(s).", new { areas = updated });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4470c7a41470fb841b1d7e75d990eafd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user