-
Notifications
You must be signed in to change notification settings - Fork 922
feat: add manage_profiler tool for CPU timing, GC alloc, and animation profiling #991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0e63704
feat: add manage_profiler tool for CPU timing, GC alloc, and animatio…
zaferdace f503d7b
fix: correct ProfilerRecorder counter names in ScriptTiming and Physi…
zaferdace f42d268
fix: update physics counter names in manage_profiler description
zaferdace 31fa37e
fix: address review feedback — correct counter names, CLI help text, …
zaferdace 806fa0f
refactor: use explicit valid keys in COUNTER_MAP tuples
zaferdace 8925c32
fix: use official Unity Frame Timing API counter names
zaferdace File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}"); | ||
| } | ||
| } | ||
| } | ||
| } |
25 changes: 25 additions & 0 deletions
25
MCPForUnity/Editor/Tools/Profiler/Operations/AnimationTimingOps.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, object>(); | ||
|
|
||
| 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 | ||
| }; | ||
| } | ||
| } | ||
| } |
36 changes: 36 additions & 0 deletions
36
MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 valueKey, string validKey)[] COUNTER_MAP = new[] | ||
| { | ||
| ("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"), | ||
| }; | ||
|
|
||
| internal static object GetFrameTiming(JObject @params) | ||
| { | ||
| var data = new Dictionary<string, object>(); | ||
|
|
||
| foreach (var (counterName, valueKey, validKey) in COUNTER_MAP) | ||
| { | ||
| using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Internal, counterName); | ||
| data[valueKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; | ||
| data[validKey] = recorder.Valid; | ||
| } | ||
|
|
||
| return new | ||
| { | ||
| success = true, | ||
| message = "Frame timing captured.", | ||
| data | ||
| }; | ||
| } | ||
| } | ||
| } |
33 changes: 33 additions & 0 deletions
33
MCPForUnity/Editor/Tools/Profiler/Operations/GCAllocOps.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, object>(); | ||
|
|
||
| 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 | ||
| }; | ||
| } | ||
| } | ||
| } |
34 changes: 34 additions & 0 deletions
34
MCPForUnity/Editor/Tools/Profiler/Operations/PhysicsTimingOps.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 valueKey, string validKey)[] COUNTER_MAP = new[] | ||
| { | ||
| ("Physics.Simulate", "simulate_ms", "simulate_valid"), | ||
| ("Physics2D.Simulate", "simulate_2d_ms", "simulate_2d_valid"), | ||
| }; | ||
|
|
||
| internal static object GetPhysicsTiming(JObject @params) | ||
| { | ||
| var data = new Dictionary<string, object>(); | ||
|
|
||
| foreach (var (counterName, valueKey, validKey) in COUNTER_MAP) | ||
| { | ||
| using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Physics, counterName); | ||
| data[valueKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; | ||
| data[validKey] = recorder.Valid; | ||
| } | ||
|
|
||
| return new | ||
| { | ||
| success = true, | ||
| message = "Physics timing captured.", | ||
| data | ||
| }; | ||
| } | ||
| } | ||
| } |
35 changes: 35 additions & 0 deletions
35
MCPForUnity/Editor/Tools/Profiler/Operations/ScriptTimingOps.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 valueKey, string validKey)[] COUNTER_MAP = new[] | ||
| { | ||
| ("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<string, object>(); | ||
|
|
||
| foreach (var (counterName, valueKey, validKey) in COUNTER_MAP) | ||
| { | ||
| using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Scripts, counterName); | ||
| data[valueKey] = recorder.Valid ? recorder.CurrentValue / 1e6 : 0.0; | ||
| data[validKey] = recorder.Valid; | ||
| } | ||
|
|
||
| return new | ||
| { | ||
| success = true, | ||
| message = "Script timing captured.", | ||
| data | ||
| }; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.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)) | ||
|
|
||
|
|
||
| @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)) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.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" | ||
| "- 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)} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.