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 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() }); // Start recorders var recorders = new List(); foreach (string name in counterNames) { recorders.Add(ProfilerRecorder.StartNew(category, name)); } var data = new Dictionary(); 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 GetRequestedCounters(ToolParams p, ProfilerCategory category) { var explicitCounters = p.GetStringArray("counters"); if (explicitCounters != null && explicitCounters.Length > 0) return explicitCounters.ToList(); var allHandles = new List(); 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(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; } } } }