using System;
using System.Collections.Generic;
using System.Diagnostics;
using S3.Modules.Profiler;
using UnityEngine;
namespace S3.Mcp.Tools;
///
/// Low-allocation manual sampler. It stores full probe detail only for hitch
/// frames and ignores sub-0.1ms samples, avoiding profiler-induced GC stalls.
///
internal static class SparseHitchSampler
{
static readonly Dictionary Current =
new(StringComparer.Ordinal);
static readonly List 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();
}
public static List End()
{
Active = false;
DestroyDriver();
Current.Clear();
_pending = false;
return new List(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);
}