using System.Diagnostics; using System.Reactive.Disposables; using System.Runtime.CompilerServices; using System.Text; namespace StabilityMatrix.Core.Helper; public class CodeTimer : IDisposable { private static readonly Stack RunningTimers = new(); private readonly string name; private readonly Stopwatch stopwatch; private CodeTimer? ParentTimer { get; } private List SubTimers { get; } = new(); public CodeTimer(string postFix = "", [CallerMemberName] string callerName = "") { name = $"{callerName}" + (string.IsNullOrEmpty(postFix) ? "" : $" ({postFix})"); stopwatch = Stopwatch.StartNew(); // Set parent as the top of the stack if (RunningTimers.TryPeek(out var timer)) { ParentTimer = timer; timer.SubTimers.Add(this); } // Add ourselves to the stack RunningTimers.Push(this); } /// /// Starts a new timer and returns it if DEBUG is defined, otherwise returns an empty IDisposable /// /// /// /// public static IDisposable StartDebug( string postFix = "", [CallerMemberName] string callerName = "" ) { #if DEBUG return new CodeTimer(postFix, callerName); #else return Disposable.Empty; #endif } /// /// Formats a TimeSpan into a string. Chooses the most appropriate unit of time. /// private static string FormatTime(TimeSpan duration) { if (duration.TotalSeconds < 1) { return $"{duration.TotalMilliseconds:0.00}ms"; } if (duration.TotalMinutes < 1) { return $"{duration.TotalSeconds:0.00}s"; } if (duration.TotalHours < 1) { return $"{duration.TotalMinutes:0.00}m"; } return $"{duration.TotalHours:0.00}h"; } private static void OutputDebug(string message) { Debug.WriteLine(message); } /// /// Get results for this timer and all sub timers recursively /// private string GetResult() { var builder = new StringBuilder(); builder.AppendLine($"{name}: took {FormatTime(stopwatch.Elapsed)}"); foreach (var timer in SubTimers) { // For each sub timer layer, add a `|-` prefix builder.AppendLine($"|- {timer.GetResult()}"); } return builder.ToString(); } public void Dispose() { stopwatch.Stop(); // Remove ourselves from the stack if (RunningTimers.TryPop(out var timer)) { if (timer != this) { throw new InvalidOperationException("Timer stack is corrupted"); } } else { throw new InvalidOperationException("Timer stack is empty"); } // If we're a root timer, output all results if (ParentTimer is null) { OutputDebug(GetResult()); SubTimers.Clear(); } GC.SuppressFinalize(this); } }