using AlicizaX.Editor; using AlicizaX.Timer.Runtime; using UnityEditor; using UnityEngine; namespace AlicizaX.Timer.Editor { [CustomEditor(typeof(TimerComponent))] internal sealed class TimerComponentInspector : GameFrameworkInspector { private const double UPDATE_INTERVAL = 0.25d; private const int MAX_DISPLAY_COUNT = 20; private TimerDebugInfo[] _timerBuffer; #if UNITY_EDITOR private TimerDebugInfo[] _leakBuffer; #endif private double _lastUpdateTime; private int _cachedActiveCount; private int _cachedPoolCapacity; private int _cachedPeakActiveCount; private int _cachedFreeCount; private string _cachedUsageText; public override void OnInspectorGUI() { base.OnInspectorGUI(); serializedObject.Update(); serializedObject.ApplyModifiedProperties(); DrawRuntimeDebugInfo(); if (EditorApplication.isPlaying) { double currentTime = EditorApplication.timeSinceStartup; if (currentTime - _lastUpdateTime >= UPDATE_INTERVAL) { _lastUpdateTime = currentTime; Repaint(); } } } private void DrawRuntimeDebugInfo() { if (!EditorApplication.isPlaying) { EditorGUILayout.HelpBox("Available during runtime only.", MessageType.Info); return; } if (!AppServices.TryGet(out ITimerService timerService)) { EditorGUILayout.HelpBox("Timer service is not initialized.", MessageType.Info); return; } if (timerService is not ITimerServiceDebug debugView) { return; } debugView.GetStatistics(out _cachedActiveCount, out _cachedPoolCapacity, out _cachedPeakActiveCount, out _cachedFreeCount); _cachedUsageText = _cachedPoolCapacity > 0 ? Utility.Text.Format("{0:F1}%", (float)_cachedActiveCount / _cachedPoolCapacity * 100f) : "0.0%"; EditorGUILayout.Space(); EditorGUILayout.LabelField("Runtime Debug", EditorStyles.boldLabel); EditorGUILayout.LabelField("Active Timers", _cachedActiveCount.ToString()); EditorGUILayout.LabelField("Pool Capacity", _cachedPoolCapacity.ToString()); EditorGUILayout.LabelField("Peak Active Count", _cachedPeakActiveCount.ToString()); EditorGUILayout.LabelField("Free Slots", _cachedFreeCount.ToString()); EditorGUILayout.LabelField("Pool Usage", _cachedUsageText); DrawTimerList(debugView, _cachedActiveCount); #if UNITY_EDITOR DrawLeakDetection(debugView, _cachedActiveCount); #endif } private void DrawTimerList(ITimerServiceDebug debug, int activeCount) { EditorGUILayout.Space(); EditorGUILayout.LabelField("Active Timers", EditorStyles.boldLabel); if (activeCount <= 0) { EditorGUILayout.LabelField("No active timers."); return; } EnsureTimerBuffer(activeCount); int timerCount = debug.GetAllTimers(_timerBuffer); int displayCount = Mathf.Min(timerCount, MAX_DISPLAY_COUNT); if (displayCount < activeCount) { EditorGUILayout.HelpBox(Utility.Text.Format("Showing first {0} timers of {1}.", displayCount, activeCount), MessageType.Info); } for (int i = 0; i < displayCount; i++) { TimerDebugInfo timer = _timerBuffer[i]; bool isLoop = (timer.Flags & TimerDebugFlags.Loop) != 0; bool isRunning = (timer.Flags & TimerDebugFlags.Running) != 0; bool isUnscaled = (timer.Flags & TimerDebugFlags.Unscaled) != 0; string label = Utility.Text.Format( "ID {0} | {1} | {2} | {3}", timer.TimerHandle, isLoop ? "Loop" : "Once", isUnscaled ? "Unscaled" : "Scaled", isRunning ? "Running" : "Paused"); string value = Utility.Text.Format( "Left {0:F2}s | Duration {1:F2}s", timer.LeftTime, timer.Duration); EditorGUILayout.LabelField(label, value); } } #if UNITY_EDITOR private void DrawLeakDetection(ITimerServiceDebug debug, int activeCount) { if (activeCount <= 0) { return; } EnsureLeakBuffer(activeCount); int staleCount = debug.GetStaleOneShotTimers(_leakBuffer); if (staleCount <= 0) { return; } EditorGUILayout.Space(); EditorGUILayout.LabelField(Utility.Text.Format("Stale One-Shot Timers ({0})", staleCount), EditorStyles.boldLabel); EditorGUILayout.HelpBox("Non-loop timers older than 5 minutes. This may indicate long-delay tasks or paused timers.", MessageType.Warning); for (int i = 0; i < staleCount; i++) { TimerDebugInfo staleTimer = _leakBuffer[i]; EditorGUILayout.LabelField( Utility.Text.Format("ID {0}", staleTimer.TimerHandle), Utility.Text.Format("Created {0:F1}s ago", staleTimer.Age)); } } #endif private void EnsureTimerBuffer(int count) { int capacity = count > 0 ? count : 1; if (_timerBuffer == null || _timerBuffer.Length < capacity) { _timerBuffer = new TimerDebugInfo[capacity]; } } #if UNITY_EDITOR private void EnsureLeakBuffer(int count) { int capacity = count > 0 ? count : 1; if (_leakBuffer == null || _leakBuffer.Length < capacity) { _leakBuffer = new TimerDebugInfo[capacity]; } } #endif } }