Loopback MCP host with observe/control/develop gates and a hot-reload tools DLL. Default token is local-dev only. Leave the module off unless you are driving S3 from an agent.
197 lines
7 KiB
C#
197 lines
7 KiB
C#
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;
|
|
|
|
/// <summary>Capture-scoped Unity PlayerLoop and subsystem marker recorders.</summary>
|
|
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<Entry> Entries = new();
|
|
static GameObject? _driverObject;
|
|
|
|
public static int ActiveMarkers => Entries.Count;
|
|
|
|
public static string Start()
|
|
{
|
|
Stop();
|
|
var names = new List<string>();
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
var available = new List<ProfilerRecorderHandle>();
|
|
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<UnityMarkerFrameDriver>();
|
|
}
|
|
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<string> names,
|
|
HashSet<string> 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();
|
|
}
|