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.
145 lines
4.2 KiB
C#
145 lines
4.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using S3.Modules.Profiler;
|
|
using UnityEngine;
|
|
|
|
namespace S3.Mcp.Tools;
|
|
|
|
/// <summary>
|
|
/// Low-allocation manual sampler. It stores full probe detail only for hitch
|
|
/// frames and ignores sub-0.1ms samples, avoiding profiler-induced GC stalls.
|
|
/// </summary>
|
|
internal static class SparseHitchSampler
|
|
{
|
|
static readonly Dictionary<string, HitchProbeSample> Current =
|
|
new(StringComparer.Ordinal);
|
|
static readonly List<HitchFrameRecord> Frames = new(4096);
|
|
static GameObject? _driverObject;
|
|
static float _threshold;
|
|
static int _frame;
|
|
static int _gc0;
|
|
static int _gc1;
|
|
static int _gc2;
|
|
static long _mono;
|
|
static bool _pending;
|
|
|
|
public static bool Active { get; private set; }
|
|
|
|
public static void Begin(float thresholdMs)
|
|
{
|
|
Cancel();
|
|
Current.Clear();
|
|
Frames.Clear();
|
|
_threshold = thresholdMs;
|
|
_frame = 0;
|
|
_gc0 = GC.CollectionCount(0);
|
|
_gc1 = GC.CollectionCount(1);
|
|
_gc2 = GC.CollectionCount(2);
|
|
_mono = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|
_pending = false;
|
|
Active = true;
|
|
_driverObject = new GameObject("S3 Sparse Hitch Sampler");
|
|
_driverObject.hideFlags = HideFlags.HideAndDontSave;
|
|
UnityEngine.Object.DontDestroyOnLoad(_driverObject);
|
|
_driverObject.AddComponent<SparseHitchFrameDriver>();
|
|
}
|
|
|
|
public static List<HitchFrameRecord> End()
|
|
{
|
|
Active = false;
|
|
DestroyDriver();
|
|
Current.Clear();
|
|
_pending = false;
|
|
return new List<HitchFrameRecord>(Frames);
|
|
}
|
|
|
|
public static void Cancel()
|
|
{
|
|
Active = false;
|
|
DestroyDriver();
|
|
Current.Clear();
|
|
Frames.Clear();
|
|
_pending = false;
|
|
}
|
|
|
|
public static void Record(string id, long ticks, int calls = 1)
|
|
{
|
|
if (!Active || ticks <= Stopwatch.Frequency / 10_000) return;
|
|
if (!Current.TryGetValue(id, out HitchProbeSample? sample))
|
|
{
|
|
sample = new HitchProbeSample();
|
|
Current[id] = sample;
|
|
}
|
|
sample.Calls += calls;
|
|
sample.Ticks += ticks;
|
|
if (ticks > sample.MaxTicks) sample.MaxTicks = ticks;
|
|
}
|
|
|
|
public static void Advance(float frameMs)
|
|
{
|
|
if (!Active) return;
|
|
int gc0 = GC.CollectionCount(0);
|
|
int gc1 = GC.CollectionCount(1);
|
|
int gc2 = GC.CollectionCount(2);
|
|
long mono = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|
|
|
if (_pending && frameMs > 0f && frameMs < 2000f)
|
|
{
|
|
var frame = new HitchFrameRecord
|
|
{
|
|
Phase = "manual-sparse",
|
|
Frame = _frame++,
|
|
FrameMs = frameMs,
|
|
Gc0 = gc0 - _gc0,
|
|
Gc1 = gc1 - _gc1,
|
|
Gc2 = gc2 - _gc2,
|
|
MonoDelta = mono - _mono,
|
|
CameraPosition = Camera.main != null
|
|
? Camera.main.transform.position
|
|
: Vector3.zero,
|
|
};
|
|
if (frameMs >= _threshold)
|
|
{
|
|
foreach (var pair in Current)
|
|
{
|
|
HitchProbeSample sample = pair.Value;
|
|
if (sample.Calls == 0) continue;
|
|
frame.Probes[pair.Key] = new HitchProbeSample
|
|
{
|
|
Calls = sample.Calls,
|
|
Ticks = sample.Ticks,
|
|
MaxTicks = sample.MaxTicks,
|
|
};
|
|
}
|
|
}
|
|
Frames.Add(frame);
|
|
}
|
|
|
|
foreach (HitchProbeSample sample in Current.Values)
|
|
{
|
|
sample.Calls = 0;
|
|
sample.Ticks = 0;
|
|
sample.MaxTicks = 0;
|
|
}
|
|
_gc0 = gc0;
|
|
_gc1 = gc1;
|
|
_gc2 = gc2;
|
|
_mono = mono;
|
|
_pending = true;
|
|
}
|
|
|
|
static void DestroyDriver()
|
|
{
|
|
if (_driverObject == null) return;
|
|
UnityEngine.Object.Destroy(_driverObject);
|
|
_driverObject = null;
|
|
}
|
|
}
|
|
|
|
[DefaultExecutionOrder(-31999)]
|
|
public sealed class SparseHitchFrameDriver : MonoBehaviour
|
|
{
|
|
void Update() =>
|
|
SparseHitchSampler.Advance(Time.unscaledDeltaTime * 1000f);
|
|
}
|