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.
116 lines
3.5 KiB
C#
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;
|
|
}
|
|
}
|