using System; using System.Collections.Generic; using System.Diagnostics; using S3.Modules.Profiler; using Unity.Profiling; using Unity.Profiling.LowLevel.Unsafe; using UnityEngine; namespace S3.Mcp.Tools; /// Capture-scoped Unity PlayerLoop and subsystem marker recorders. internal static class UnityMarkerProfiler { sealed class Entry { public string Id = ""; public ProfilerRecorder Recorder; } readonly struct Marker { public readonly ProfilerCategory Category; public readonly string Name; public Marker(ProfilerCategory category, string name) { Category = category; Name = name; } } static readonly Marker[] CandidateMarkers = { new(ProfilerCategory.Scripts, "BehaviourUpdate"), new(ProfilerCategory.Scripts, "ScriptRunBehaviourUpdate"), new(ProfilerCategory.Scripts, "ScriptRunBehaviourFixedUpdate"), new(ProfilerCategory.Scripts, "ScriptRunBehaviourLateUpdate"), new(ProfilerCategory.Scripts, "ScriptRunDelayedTasks"), new(ProfilerCategory.Scripts, "CoroutinesDelayedCalls"), new(ProfilerCategory.Scripts, "GC.Collect"), new(ProfilerCategory.Scripts, "GC.Incremental.Collect"), new(ProfilerCategory.Internal, "PlayerLoop"), new(ProfilerCategory.Internal, "EarlyUpdate"), new(ProfilerCategory.Internal, "FixedUpdate"), new(ProfilerCategory.Internal, "PreUpdate"), new(ProfilerCategory.Internal, "Update"), new(ProfilerCategory.Internal, "PreLateUpdate"), new(ProfilerCategory.Internal, "PostLateUpdate"), new(ProfilerCategory.Internal, "WaitForJobGroupID"), new(ProfilerCategory.Internal, "JobHandle.Complete"), new(ProfilerCategory.Internal, "Semaphore.WaitForSignal"), new(ProfilerCategory.Internal, "WaitForTargetFPS"), new(ProfilerCategory.Internal, "Gfx.WaitForPresentOnGfxThread"), new(ProfilerCategory.Internal, "Gfx.PresentFrame"), new(ProfilerCategory.Loading, "PreloadManager.UpdatePreloading"), new(ProfilerCategory.Loading, "Application.Integrate Assets in Background"), new(ProfilerCategory.Loading, "AsyncUploadManager.Update"), new(ProfilerCategory.Loading, "Resources.UnloadUnusedAssets"), new(ProfilerCategory.Loading, "SceneManager.Update"), new(ProfilerCategory.Render, "UpdateAllRenderers"), new(ProfilerCategory.Render, "UpdateRendererBoundingVolumes"), new(ProfilerCategory.Render, "UpdateAllSkinnedMeshes"), new(ProfilerCategory.Render, "Camera.Render"), new(ProfilerCategory.Render, "CullScriptable"), new(ProfilerCategory.Render, "RenderLoop.Draw"), new(ProfilerCategory.Render, "BatchRendererGroup"), new(ProfilerCategory.Animation, "DirectorUpdateAnimationBegin"), new(ProfilerCategory.Animation, "DirectorUpdateAnimationEnd"), new(ProfilerCategory.Animation, "Animator.Update"), new(ProfilerCategory.Physics, "Physics.Simulate"), new(ProfilerCategory.Particles, "ParticleSystem.Update"), new(ProfilerCategory.Particles, "ParticleSystem.ScheduleGeometryJobs"), }; static readonly List Entries = new(); static GameObject? _driverObject; public static int ActiveMarkers => Entries.Count; public static string Start() { Stop(); var names = new List(); var seen = new HashSet(StringComparer.Ordinal); var available = new List(); try { ProfilerRecorderHandle.GetAvailable(available); } catch { } for (int i = 0; i < available.Count && Entries.Count < 512; i++) { try { ProfilerRecorderDescription description = ProfilerRecorderHandle.GetDescription(available[i]); string name = description.Name; if (description.UnitType != ProfilerMarkerDataUnit.TimeNanoseconds) continue; AddRecorder(description.Category, name, names, seen); } catch { } } // Some release players expose only a small built-in counter list. // Probe known marker/category pairs to supplement that list. for (int i = 0; i < CandidateMarkers.Length && Entries.Count < 512; i++) { Marker marker = CandidateMarkers[i]; AddRecorder(marker.Category, marker.Name, names, seen); } if (Entries.Count > 0) { _driverObject = new GameObject("S3 Deep Unity Marker Profiler"); _driverObject.hideFlags = HideFlags.HideAndDontSave; UnityEngine.Object.DontDestroyOnLoad(_driverObject); _driverObject.AddComponent(); } return Entries.Count == 0 ? "markers=0" : $"available={available.Count} markers={Entries.Count} " + $"[{string.Join(", ", names.GetRange(0, Math.Min(20, names.Count)))}" + (names.Count > 20 ? ", ...]" : "]"); } static void AddRecorder( ProfilerCategory category, string name, List names, HashSet seen) { string unique = category.Name + ":" + name; if (!seen.Add(unique)) return; try { ProfilerRecorder recorder = ProfilerRecorder.StartNew( category, name, 1, ProfilerRecorderOptions.StartImmediately | ProfilerRecorderOptions.SumAllSamplesInFrame); if (!recorder.Valid) { recorder.Dispose(); return; } string id = "unity." + category.Name + ":" + name; Entries.Add(new Entry { Id = id, Recorder = recorder }); names.Add(unique); } catch { } } public static void Stop() { if (_driverObject != null) { UnityEngine.Object.Destroy(_driverObject); _driverObject = null; } for (int i = 0; i < Entries.Count; i++) { try { Entries[i].Recorder.Dispose(); } catch { } } Entries.Clear(); } public static void Sample() { if (!SparseHitchSampler.Active) return; for (int i = 0; i < Entries.Count; i++) { Entry entry = Entries[i]; long nanoseconds; try { nanoseconds = entry.Recorder.LastValue; } catch { continue; } if (nanoseconds <= 0 || nanoseconds > 10_000_000_000L) continue; long ticks = (long)( nanoseconds * (double)Stopwatch.Frequency / 1_000_000_000.0); SparseHitchSampler.Record(entry.Id, ticks); } } } [DefaultExecutionOrder(31900)] public sealed class UnityMarkerFrameDriver : MonoBehaviour { void LateUpdate() => UnityMarkerProfiler.Sample(); }