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); }