railroader-setons-special-s.../mcp-tools/PlayerLoopProfiler.cs
seton 5e18cb84a1 MCP: localhost agent server with reloadable S3.Mcp.Tools plugin pack
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.
2026-09-11 15:19:24 -04:00

116 lines
3.5 KiB
C#

using System;
using System.Diagnostics;
using S3.Modules.Profiler;
using UnityEngine.LowLevel;
namespace S3.Mcp.Tools;
/// <summary>
/// Temporarily inserts timestamp boundaries around every Unity PlayerLoop
/// subsystem. This measures native engine phases that Harmony cannot patch.
/// </summary>
internal static class PlayerLoopProfiler
{
static PlayerLoopSystem _original;
static long[] _depthStarts = Array.Empty<long>();
public static bool Active { get; private set; }
public static int Boundaries { get; private set; }
public static string Start()
{
Stop();
try
{
_original = PlayerLoop.GetCurrentPlayerLoop();
PlayerLoopSystem instrumented = _original;
Boundaries = 0;
int maxDepth = MeasureDepth(instrumented, 0);
_depthStarts = new long[Math.Max(2, maxDepth + 2)];
Instrument(ref instrumented, 0, "PlayerLoop");
PlayerLoop.SetPlayerLoop(instrumented);
Active = true;
return $"boundaries={Boundaries} depth={maxDepth}";
}
catch (Exception ex)
{
Stop();
return "failed:" + ex.GetType().Name;
}
}
public static void Stop()
{
if (Active)
{
try { PlayerLoop.SetPlayerLoop(_original); }
catch { }
}
Active = false;
Boundaries = 0;
_depthStarts = Array.Empty<long>();
}
static int MeasureDepth(PlayerLoopSystem system, int depth)
{
int max = depth;
PlayerLoopSystem[]? children = system.subSystemList;
if (children == null) return max;
for (int i = 0; i < children.Length; i++)
max = Math.Max(max, MeasureDepth(children[i], depth + 1));
return max;
}
static void Instrument(
ref PlayerLoopSystem system,
int depth,
string parentPath)
{
PlayerLoopSystem[]? children = system.subSystemList;
if (children == null || children.Length == 0) return;
var expanded = new PlayerLoopSystem[children.Length * 2 + 1];
expanded[0] = Boundary(depth, null);
for (int i = 0; i < children.Length; i++)
{
PlayerLoopSystem child = children[i];
string childName = DisplayName(child.type, i);
string path = parentPath + "." + childName;
Instrument(ref child, depth + 1, path);
expanded[i * 2 + 1] = child;
expanded[i * 2 + 2] = Boundary(depth, "loop." + path);
Boundaries++;
}
system.subSystemList = expanded;
}
static PlayerLoopSystem Boundary(int depth, string? completedId)
{
return new PlayerLoopSystem
{
type = typeof(PlayerLoopProfiler),
updateDelegate = () => Mark(depth, completedId),
};
}
static void Mark(int depth, string? completedId)
{
long now = Stopwatch.GetTimestamp();
if (completedId != null &&
SparseHitchSampler.Active &&
depth < _depthStarts.Length)
{
long start = _depthStarts[depth];
if (start != 0 && now >= start)
SparseHitchSampler.Record(completedId, now - start);
}
if (depth < _depthStarts.Length)
_depthStarts[depth] = now;
}
static string DisplayName(Type? type, int index)
{
if (type == null) return "unnamed" + index;
return type.FullName ?? type.Name;
}
}