From 0e63704ff233c26b6588d6a725aaeb543a603ba1 Mon Sep 17 00:00:00 2001 From: zaferdace <47742545+zaferdace@users.noreply.github.com> Date: Thu, 26 Mar 2026 07:14:22 +0000 Subject: [PATCH 1/6] feat: add manage_profiler tool for CPU timing, GC alloc, and animation profiling Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Editor/Tools/Profiler/ManageProfiler.cs | 49 ++++ .../Profiler/Operations/AnimationTimingOps.cs | 25 +++ .../Profiler/Operations/FrameTimingOps.cs | 36 +++ .../Tools/Profiler/Operations/GCAllocOps.cs | 33 +++ .../Profiler/Operations/PhysicsTimingOps.cs | 34 +++ .../Profiler/Operations/ScriptTimingOps.cs | 35 +++ Server/src/cli/commands/profiler.py | 54 +++++ Server/src/services/tools/manage_profiler.py | 57 +++++ Server/tests/test_manage_profiler.py | 209 ++++++++++++++++++ 9 files changed, 532 insertions(+) create mode 100644 MCPForUnity/Editor/Tools/Profiler/ManageProfiler.cs create mode 100644 MCPForUnity/Editor/Tools/Profiler/Operations/AnimationTimingOps.cs create mode 100644 MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs create mode 100644 MCPForUnity/Editor/Tools/Profiler/Operations/GCAllocOps.cs create mode 100644 MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs create mode 100644 MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs create mode 100644 Server/src/cli/commands/profiler.py create mode 100644 Server/src/services/tools/manage_profiler.py create mode 100644 Server/tests/test_manage_profiler.py diff --git a/MCPForUnity/Editor/Tools/Profiler/ManageProfiler.cs b/MCPForUnity/Editor/Tools/Profiler/ManageProfiler.cs new file mode 100644 index 000000000..38240e763 --- /dev/null +++ b/MCPForUnity/Editor/Tools/Profiler/ManageProfiler.cs @@ -0,0 +1,49 @@ +using System; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; + +namespace MCPForUnity.Editor.Tools.Profiler +{ + [McpForUnityTool("manage_profiler", AutoRegister = false, Group = "core")] + public static class ManageProfiler + { + public static 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) + { + case "get_frame_timing": + return FrameTimingOps.GetFrameTiming(@params); + case "get_script_timing": + return ScriptTimingOps.GetScriptTiming(@params); + case "get_physics_timing": + return PhysicsTimingOps.GetPhysicsTiming(@params); + case "get_gc_alloc": + return GCAllocOps.GetGCAlloc(@params); + case "get_animation_timing": + return AnimationTimingOps.GetAnimationTiming(@params); + default: + return new ErrorResponse( + $"Unknown action: '{action}'. Valid actions: " + + "get_frame_timing, get_script_timing, get_physics_timing, " + + "get_gc_alloc, get_animation_timing."); + } + } + catch (Exception ex) + { + McpLog.Error($"[ManageProfiler] Action '{action}' failed: {ex}"); + return new ErrorResponse($"Error in action '{action}': {ex.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/AnimationTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/AnimationTimingOps.cs new file mode 100644 index 000000000..b928cc1c6 --- /dev/null +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/AnimationTimingOps.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Unity.Profiling; + +namespace MCPForUnity.Editor.Tools.Profiler +{ + internal static class AnimationTimingOps + { + internal static object GetAnimationTiming(JObject @params) + { + var data = new Dictionary(); + + using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Animation, "Animator.Update"); + data["animator_update_ms"] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; + data["animator_update_valid"] = recorder.Valid; + + return new + { + success = true, + message = "Animation timing captured.", + data + }; + } + } +} diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs new file mode 100644 index 000000000..2be41ca99 --- /dev/null +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Unity.Profiling; + +namespace MCPForUnity.Editor.Tools.Profiler +{ + internal static class FrameTimingOps + { + private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new[] + { + ("Main Thread", "main_thread_ms"), + ("Render Thread", "render_thread_ms"), + ("CPU Frame Time", "cpu_frame_ms"), + ("GPU Frame Time", "gpu_frame_ms"), + }; + + internal static object GetFrameTiming(JObject @params) + { + var data = new Dictionary(); + + foreach (var (counterName, jsonKey) in COUNTER_MAP) + { + using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Internal, counterName); + data[jsonKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; + data[jsonKey.Replace("_ms", "_valid")] = recorder.Valid; + } + + return new + { + success = true, + message = "Frame timing captured.", + data + }; + } + } +} diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/GCAllocOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/GCAllocOps.cs new file mode 100644 index 000000000..12bf29471 --- /dev/null +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/GCAllocOps.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Unity.Profiling; + +namespace MCPForUnity.Editor.Tools.Profiler +{ + internal static class GCAllocOps + { + internal static object GetGCAlloc(JObject @params) + { + var data = new Dictionary(); + + using (var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC.Alloc")) + { + data["gc_alloc_bytes"] = recorder.Valid ? recorder.CurrentValue : 0L; + data["gc_alloc_valid"] = recorder.Valid; + } + + using (var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC.Alloc.Count")) + { + data["gc_alloc_count"] = recorder.Valid ? recorder.CurrentValue : 0L; + data["gc_alloc_count_valid"] = recorder.Valid; + } + + return new + { + success = true, + message = "GC allocation stats captured.", + data + }; + } + } +} diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs new file mode 100644 index 000000000..f25bf7a13 --- /dev/null +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Unity.Profiling; + +namespace MCPForUnity.Editor.Tools.Profiler +{ + internal static class PhysicsTimingOps + { + private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new[] + { + ("Physics.Processing", "processing_ms"), + ("Physics.FetchResults", "fetch_results_ms"), + }; + + internal static object GetPhysicsTiming(JObject @params) + { + var data = new Dictionary(); + + foreach (var (counterName, jsonKey) in COUNTER_MAP) + { + using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Physics, counterName); + data[jsonKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; + data[jsonKey.Replace("_ms", "_valid")] = recorder.Valid; + } + + return new + { + success = true, + message = "Physics timing captured.", + data + }; + } + } +} diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs new file mode 100644 index 000000000..2c893241f --- /dev/null +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Unity.Profiling; + +namespace MCPForUnity.Editor.Tools.Profiler +{ + internal static class ScriptTimingOps + { + private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new[] + { + ("Update.ScriptRunBehaviourUpdate", "update_ms"), + ("FixedBehaviourUpdate", "fixed_update_ms"), + ("PreLateUpdate.ScriptRunBehaviourLateUpdate", "late_update_ms"), + }; + + internal static object GetScriptTiming(JObject @params) + { + var data = new Dictionary(); + + foreach (var (counterName, jsonKey) in COUNTER_MAP) + { + using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Scripts, counterName); + data[jsonKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; + data[jsonKey.Replace("_ms", "_valid")] = recorder.Valid; + } + + return new + { + success = true, + message = "Script timing captured.", + data + }; + } + } +} diff --git a/Server/src/cli/commands/profiler.py b/Server/src/cli/commands/profiler.py new file mode 100644 index 000000000..f8fe579aa --- /dev/null +++ b/Server/src/cli/commands/profiler.py @@ -0,0 +1,54 @@ +import click +from cli.utils.connection import handle_unity_errors, run_command, get_config +from cli.utils.output import format_output + + +@click.group("profiler") +def profiler(): + """Read Unity Profiler counters: CPU timing, GC allocation, and animation.""" + pass + + +@profiler.command("frame-timing") +@handle_unity_errors +def frame_timing(): + """Get main thread, render thread, CPU and GPU frame timing (ms).""" + config = get_config() + result = run_command("manage_profiler", {"action": "get_frame_timing"}, config) + click.echo(format_output(result, config.format)) + + +@profiler.command("script-timing") +@handle_unity_errors +def script_timing(): + """Get Update, FixedUpdate, and LateUpdate script execution time (ms).""" + config = get_config() + result = run_command("manage_profiler", {"action": "get_script_timing"}, config) + click.echo(format_output(result, config.format)) + + +@profiler.command("physics-timing") +@handle_unity_errors +def physics_timing(): + """Get Physics.Processing and Physics.FetchResults time (ms).""" + config = get_config() + result = run_command("manage_profiler", {"action": "get_physics_timing"}, config) + click.echo(format_output(result, config.format)) + + +@profiler.command("gc-alloc") +@handle_unity_errors +def gc_alloc(): + """Get GC allocation bytes and count per frame.""" + config = get_config() + result = run_command("manage_profiler", {"action": "get_gc_alloc"}, config) + click.echo(format_output(result, config.format)) + + +@profiler.command("animation-timing") +@handle_unity_errors +def animation_timing(): + """Get Animator.Update time (ms).""" + config = get_config() + result = run_command("manage_profiler", {"action": "get_animation_timing"}, config) + click.echo(format_output(result, config.format)) diff --git a/Server/src/services/tools/manage_profiler.py b/Server/src/services/tools/manage_profiler.py new file mode 100644 index 000000000..350e0b71c --- /dev/null +++ b/Server/src/services/tools/manage_profiler.py @@ -0,0 +1,57 @@ +from typing import Annotated, Any + +from fastmcp import Context +from mcp.types import ToolAnnotations + +from services.registry import mcp_for_unity_tool +from services.tools import get_unity_instance_from_context +from transport.unity_transport import send_with_unity_instance +from transport.legacy.unity_connection import async_send_command_with_retry + +PROFILER_ACTIONS = [ + "get_frame_timing", + "get_script_timing", + "get_physics_timing", + "get_gc_alloc", + "get_animation_timing", +] + + +@mcp_for_unity_tool( + group="core", + description=( + "Read Unity Profiler counters for CPU timing, GC allocation, and animation.\n\n" + "FRAME TIMING:\n" + "- get_frame_timing: Main thread, render thread, CPU frame time, GPU frame time (ms)\n\n" + "SCRIPT TIMING:\n" + "- get_script_timing: Update, FixedUpdate, LateUpdate script execution time (ms)\n\n" + "PHYSICS TIMING:\n" + "- get_physics_timing: Physics.Processing, Physics.FetchResults time (ms)\n\n" + "GC ALLOCATION:\n" + "- get_gc_alloc: GC allocation bytes and count per frame\n\n" + "ANIMATION TIMING:\n" + "- get_animation_timing: Animator.Update time (ms)" + ), + annotations=ToolAnnotations( + title="Manage Profiler", + destructiveHint=False, + readOnlyHint=True, + ), +) +async def manage_profiler( + ctx: Context, + action: Annotated[str, "The profiler action to perform."], +) -> dict[str, Any]: + action_lower = action.lower() + if action_lower not in PROFILER_ACTIONS: + return { + "success": False, + "message": f"Unknown action '{action}'. Valid actions: {', '.join(PROFILER_ACTIONS)}", + } + + unity_instance = await get_unity_instance_from_context(ctx) + + result = await send_with_unity_instance( + async_send_command_with_retry, unity_instance, "manage_profiler", {"action": action_lower} + ) + return result if isinstance(result, dict) else {"success": False, "message": str(result)} diff --git a/Server/tests/test_manage_profiler.py b/Server/tests/test_manage_profiler.py new file mode 100644 index 000000000..ddd33d865 --- /dev/null +++ b/Server/tests/test_manage_profiler.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest + +from services.tools.manage_profiler import ( + manage_profiler, + PROFILER_ACTIONS, +) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def mock_unity(monkeypatch): + """Patch Unity transport layer and return captured call dict.""" + captured: dict[str, object] = {} + + async def fake_send(send_fn, unity_instance, tool_name, params): + captured["unity_instance"] = unity_instance + captured["tool_name"] = tool_name + captured["params"] = params + return {"success": True, "message": "ok"} + + monkeypatch.setattr( + "services.tools.manage_profiler.get_unity_instance_from_context", + AsyncMock(return_value="unity-instance-1"), + ) + monkeypatch.setattr( + "services.tools.manage_profiler.send_with_unity_instance", + fake_send, + ) + return captured + + +# --------------------------------------------------------------------------- +# Action list completeness +# --------------------------------------------------------------------------- + +def test_profiler_actions_count(): + assert len(PROFILER_ACTIONS) == 5 + + +def test_no_duplicate_actions(): + assert len(PROFILER_ACTIONS) == len(set(PROFILER_ACTIONS)) + + +def test_expected_actions_present(): + expected = { + "get_frame_timing", + "get_script_timing", + "get_physics_timing", + "get_gc_alloc", + "get_animation_timing", + } + assert set(PROFILER_ACTIONS) == expected + + +# --------------------------------------------------------------------------- +# Invalid / missing action +# --------------------------------------------------------------------------- + +def test_unknown_action_returns_error(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="nonexistent_action") + ) + assert result["success"] is False + assert "Unknown action" in result["message"] + assert "tool_name" not in mock_unity + + +def test_empty_action_returns_error(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="") + ) + assert result["success"] is False + + +# --------------------------------------------------------------------------- +# Each action forwards correctly +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("action_name", PROFILER_ACTIONS) +def test_every_action_forwards_to_unity(mock_unity, action_name): + """Every valid action should be forwarded to Unity without error.""" + result = asyncio.run( + manage_profiler(SimpleNamespace(), action=action_name) + ) + assert result["success"] is True + assert mock_unity["tool_name"] == "manage_profiler" + assert mock_unity["params"]["action"] == action_name + + +def test_get_frame_timing_sends_correct_params(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="get_frame_timing") + ) + assert result["success"] is True + assert mock_unity["tool_name"] == "manage_profiler" + assert mock_unity["params"] == {"action": "get_frame_timing"} + + +def test_get_script_timing_sends_correct_params(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="get_script_timing") + ) + assert result["success"] is True + assert mock_unity["params"] == {"action": "get_script_timing"} + + +def test_get_physics_timing_sends_correct_params(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="get_physics_timing") + ) + assert result["success"] is True + assert mock_unity["params"] == {"action": "get_physics_timing"} + + +def test_get_gc_alloc_sends_correct_params(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="get_gc_alloc") + ) + assert result["success"] is True + assert mock_unity["params"] == {"action": "get_gc_alloc"} + + +def test_get_animation_timing_sends_correct_params(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="get_animation_timing") + ) + assert result["success"] is True + assert mock_unity["params"] == {"action": "get_animation_timing"} + + +# --------------------------------------------------------------------------- +# Case insensitivity +# --------------------------------------------------------------------------- + +def test_action_case_insensitive(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="Get_Frame_Timing") + ) + assert result["success"] is True + assert mock_unity["params"]["action"] == "get_frame_timing" + + +def test_action_uppercase(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="GET_GC_ALLOC") + ) + assert result["success"] is True + assert mock_unity["params"]["action"] == "get_gc_alloc" + + +# --------------------------------------------------------------------------- +# Non-dict response wrapped +# --------------------------------------------------------------------------- + +def test_non_dict_response_wrapped(monkeypatch): + """When Unity returns a non-dict, it should be wrapped.""" + monkeypatch.setattr( + "services.tools.manage_profiler.get_unity_instance_from_context", + AsyncMock(return_value="unity-1"), + ) + + async def fake_send(send_fn, unity_instance, tool_name, params): + return "unexpected string response" + + monkeypatch.setattr( + "services.tools.manage_profiler.send_with_unity_instance", + fake_send, + ) + + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="get_frame_timing") + ) + assert result["success"] is False + assert "unexpected string response" in result["message"] + + +# --------------------------------------------------------------------------- +# Only action param is sent (no extra keys) +# --------------------------------------------------------------------------- + +def test_only_action_in_params(mock_unity): + result = asyncio.run( + manage_profiler(SimpleNamespace(), action="get_animation_timing") + ) + assert result["success"] is True + assert mock_unity["params"] == {"action": "get_animation_timing"} + + +# --------------------------------------------------------------------------- +# Tool registration +# --------------------------------------------------------------------------- + +def test_tool_registered_with_core_group(): + from services.registry.tool_registry import _tool_registry + + profiler_tools = [ + t for t in _tool_registry if t.get("name") == "manage_profiler" + ] + assert len(profiler_tools) == 1 + assert profiler_tools[0]["group"] == "core" From f503d7b0bdebeae553aa15293fb3376263b6ac0f Mon Sep 17 00:00:00 2001 From: zaferdace <47742545+zaferdace@users.noreply.github.com> Date: Thu, 26 Mar 2026 07:28:29 +0000 Subject: [PATCH 2/6] fix: correct ProfilerRecorder counter names in ScriptTiming and PhysicsTiming --- .../Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs | 4 ++-- .../Editor/Tools/Profiler/Operations/ScriptTimingOps.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs index f25bf7a13..1361f5fe5 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs @@ -8,8 +8,8 @@ internal static class PhysicsTimingOps { private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new[] { - ("Physics.Processing", "processing_ms"), - ("Physics.FetchResults", "fetch_results_ms"), + ("Physics.Simulate", "simulate_ms"), + ("Physics2D.Simulate", "simulate_2d_ms"), }; internal static object GetPhysicsTiming(JObject @params) diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs index 2c893241f..2996f9ee8 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs @@ -8,9 +8,9 @@ internal static class ScriptTimingOps { private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new[] { - ("Update.ScriptRunBehaviourUpdate", "update_ms"), + ("BehaviourUpdate", "update_ms"), ("FixedBehaviourUpdate", "fixed_update_ms"), - ("PreLateUpdate.ScriptRunBehaviourLateUpdate", "late_update_ms"), + ("LateBehaviourUpdate", "late_update_ms"), }; internal static object GetScriptTiming(JObject @params) From f42d2687e00fc5461e4d11406d2b4c1b449b5868 Mon Sep 17 00:00:00 2001 From: zaferdace <47742545+zaferdace@users.noreply.github.com> Date: Thu, 26 Mar 2026 07:30:50 +0000 Subject: [PATCH 3/6] fix: update physics counter names in manage_profiler description --- Server/src/services/tools/manage_profiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/services/tools/manage_profiler.py b/Server/src/services/tools/manage_profiler.py index 350e0b71c..c43060808 100644 --- a/Server/src/services/tools/manage_profiler.py +++ b/Server/src/services/tools/manage_profiler.py @@ -26,7 +26,7 @@ "SCRIPT TIMING:\n" "- get_script_timing: Update, FixedUpdate, LateUpdate script execution time (ms)\n\n" "PHYSICS TIMING:\n" - "- get_physics_timing: Physics.Processing, Physics.FetchResults time (ms)\n\n" + "- get_physics_timing: Physics.Simulate, Physics2D.Simulate time (ms)\n\n" "GC ALLOCATION:\n" "- get_gc_alloc: GC allocation bytes and count per frame\n\n" "ANIMATION TIMING:\n" From 31fa37e8a9c495db2a2c91713e1c884263abee86 Mon Sep 17 00:00:00 2001 From: zaferdace <47742545+zaferdace@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:26:20 +0000 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20correct=20counter=20names,=20CLI=20help=20text,=20s?= =?UTF-8?q?trengthen=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tools/Profiler/Operations/ScriptTimingOps.cs | 2 +- Server/src/cli/commands/profiler.py | 2 +- Server/tests/test_manage_profiler.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs index 2996f9ee8..06acaadd4 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs @@ -10,7 +10,7 @@ private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new { ("BehaviourUpdate", "update_ms"), ("FixedBehaviourUpdate", "fixed_update_ms"), - ("LateBehaviourUpdate", "late_update_ms"), + ("PreLateUpdate.ScriptRunBehaviourLateUpdate", "late_update_ms"), }; internal static object GetScriptTiming(JObject @params) diff --git a/Server/src/cli/commands/profiler.py b/Server/src/cli/commands/profiler.py index f8fe579aa..6e94da1e4 100644 --- a/Server/src/cli/commands/profiler.py +++ b/Server/src/cli/commands/profiler.py @@ -30,7 +30,7 @@ def script_timing(): @profiler.command("physics-timing") @handle_unity_errors def physics_timing(): - """Get Physics.Processing and Physics.FetchResults time (ms).""" + """Get Physics.Simulate and Physics2D.Simulate time (ms).""" config = get_config() result = run_command("manage_profiler", {"action": "get_physics_timing"}, config) click.echo(format_output(result, config.format)) diff --git a/Server/tests/test_manage_profiler.py b/Server/tests/test_manage_profiler.py index ddd33d865..802a10fb3 100644 --- a/Server/tests/test_manage_profiler.py +++ b/Server/tests/test_manage_profiler.py @@ -79,6 +79,8 @@ def test_empty_action_returns_error(mock_unity): manage_profiler(SimpleNamespace(), action="") ) assert result["success"] is False + assert "Unknown action" in result["message"] + assert "tool_name" not in mock_unity # --------------------------------------------------------------------------- @@ -96,6 +98,14 @@ def test_every_action_forwards_to_unity(mock_unity, action_name): assert mock_unity["params"]["action"] == action_name +def test_uses_unity_instance_from_context(mock_unity): + """manage_profiler should forward the context-derived Unity instance.""" + asyncio.run( + manage_profiler(SimpleNamespace(), action="get_frame_timing") + ) + assert mock_unity["unity_instance"] == "unity-instance-1" + + def test_get_frame_timing_sends_correct_params(mock_unity): result = asyncio.run( manage_profiler(SimpleNamespace(), action="get_frame_timing") From 806fa0fe1382d1d8531d90821ce098b229362c1e Mon Sep 17 00:00:00 2001 From: zaferdace <47742545+zaferdace@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:39:05 +0000 Subject: [PATCH 5/6] refactor: use explicit valid keys in COUNTER_MAP tuples --- .../Tools/Profiler/Operations/FrameTimingOps.cs | 16 ++++++++-------- .../Profiler/Operations/PhysicsTimingOps.cs | 12 ++++++------ .../Tools/Profiler/Operations/ScriptTimingOps.cs | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs index 2be41ca99..f104e5670 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs @@ -6,23 +6,23 @@ namespace MCPForUnity.Editor.Tools.Profiler { internal static class FrameTimingOps { - private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new[] + private static readonly (string counterName, string valueKey, string validKey)[] COUNTER_MAP = new[] { - ("Main Thread", "main_thread_ms"), - ("Render Thread", "render_thread_ms"), - ("CPU Frame Time", "cpu_frame_ms"), - ("GPU Frame Time", "gpu_frame_ms"), + ("Main Thread", "main_thread_ms", "main_thread_valid"), + ("Render Thread", "render_thread_ms", "render_thread_valid"), + ("CPU Frame Time", "cpu_frame_ms", "cpu_frame_valid"), + ("GPU Frame Time", "gpu_frame_ms", "gpu_frame_valid"), }; internal static object GetFrameTiming(JObject @params) { var data = new Dictionary(); - foreach (var (counterName, jsonKey) in COUNTER_MAP) + foreach (var (counterName, valueKey, validKey) in COUNTER_MAP) { using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Internal, counterName); - data[jsonKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; - data[jsonKey.Replace("_ms", "_valid")] = recorder.Valid; + data[valueKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; + data[validKey] = recorder.Valid; } return new diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs index 1361f5fe5..555840e31 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs @@ -6,21 +6,21 @@ namespace MCPForUnity.Editor.Tools.Profiler { internal static class PhysicsTimingOps { - private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new[] + private static readonly (string counterName, string valueKey, string validKey)[] COUNTER_MAP = new[] { - ("Physics.Simulate", "simulate_ms"), - ("Physics2D.Simulate", "simulate_2d_ms"), + ("Physics.Simulate", "simulate_ms", "simulate_valid"), + ("Physics2D.Simulate", "simulate_2d_ms", "simulate_2d_valid"), }; internal static object GetPhysicsTiming(JObject @params) { var data = new Dictionary(); - foreach (var (counterName, jsonKey) in COUNTER_MAP) + foreach (var (counterName, valueKey, validKey) in COUNTER_MAP) { using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Physics, counterName); - data[jsonKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; - data[jsonKey.Replace("_ms", "_valid")] = recorder.Valid; + data[valueKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; + data[validKey] = recorder.Valid; } return new diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs index 06acaadd4..94b48dd1c 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs @@ -6,22 +6,22 @@ namespace MCPForUnity.Editor.Tools.Profiler { internal static class ScriptTimingOps { - private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new[] + private static readonly (string counterName, string valueKey, string validKey)[] COUNTER_MAP = new[] { - ("BehaviourUpdate", "update_ms"), - ("FixedBehaviourUpdate", "fixed_update_ms"), - ("PreLateUpdate.ScriptRunBehaviourLateUpdate", "late_update_ms"), + ("BehaviourUpdate", "update_ms", "update_valid"), + ("FixedBehaviourUpdate", "fixed_update_ms", "fixed_update_valid"), + ("PreLateUpdate.ScriptRunBehaviourLateUpdate", "late_update_ms", "late_update_valid"), }; internal static object GetScriptTiming(JObject @params) { var data = new Dictionary(); - foreach (var (counterName, jsonKey) in COUNTER_MAP) + foreach (var (counterName, valueKey, validKey) in COUNTER_MAP) { using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Scripts, counterName); - data[jsonKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; - data[jsonKey.Replace("_ms", "_valid")] = recorder.Valid; + data[valueKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; + data[validKey] = recorder.Valid; } return new From 8925c321c2c9884d2069d2d7166e8d69dca10ed7 Mon Sep 17 00:00:00 2001 From: zaferdace <47742545+zaferdace@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:48:40 +0000 Subject: [PATCH 6/6] fix: use official Unity Frame Timing API counter names --- .../Editor/Tools/Profiler/Operations/FrameTimingOps.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs index f104e5670..45083f96e 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs @@ -8,9 +8,9 @@ internal static class FrameTimingOps { private static readonly (string counterName, string valueKey, string validKey)[] COUNTER_MAP = new[] { - ("Main Thread", "main_thread_ms", "main_thread_valid"), - ("Render Thread", "render_thread_ms", "render_thread_valid"), - ("CPU Frame Time", "cpu_frame_ms", "cpu_frame_valid"), + ("CPU Main Thread Frame Time", "main_thread_ms", "main_thread_valid"), + ("CPU Render Thread Frame Time", "render_thread_ms", "render_thread_valid"), + ("CPU Total Frame Time", "cpu_frame_ms", "cpu_frame_valid"), ("GPU Frame Time", "gpu_frame_ms", "gpu_frame_valid"), };