using System.Diagnostics; namespace Terminal.Gui.App; /// /// Manages scheduled timeouts (timed callbacks) for the application. /// /// Allows scheduling of callbacks to be invoked after a specified delay, with optional repetition. /// Timeouts are stored in a sorted list by their scheduled execution time (high-resolution ticks). /// Thread-safe for concurrent access. /// /// /// Typical usage: /// /// /// Call to schedule a callback. /// /// /// /// Call periodically (e.g., from the main loop) to execute due /// callbacks. /// /// /// /// Call to cancel a scheduled timeout. /// /// /// /// /// /// Uses for high-resolution timing instead of /// to provide microsecond-level precision and eliminate race conditions from timer resolution issues. /// public class TimedEvents : ITimedEvents { internal SortedList _timeouts = new (); private readonly object _timeoutsLockToken = new (); /// /// Gets the list of all timeouts sorted by the time ticks. A shorter limit time can be /// added at the end, but it will be called before an earlier addition that has a longer limit time. /// public SortedList Timeouts => _timeouts; /// public event EventHandler? Added; /// /// Gets the current high-resolution timestamp in TimeSpan ticks. /// Uses for microsecond-level precision. /// /// Current timestamp in TimeSpan ticks (100-nanosecond units). private static long GetTimestampTicks () { // Convert Stopwatch ticks to TimeSpan ticks (100-nanosecond units) // Stopwatch.Frequency gives ticks per second, so we need to scale appropriately // To avoid overflow, we perform the operation in double precision first and then cast to long. var ticks = (long)((double)Stopwatch.GetTimestamp () * TimeSpan.TicksPerSecond / Stopwatch.Frequency); // Ensure ticks is positive and not overflowed (very unlikely now) Debug.Assert (ticks > 0); return ticks; } /// public void RunTimers () { lock (_timeoutsLockToken) { if (_timeouts.Count > 0) { RunTimersImpl (); } } } /// public bool Remove (object token) { lock (_timeoutsLockToken) { int idx = _timeouts.IndexOfValue ((token as Timeout)!); if (idx == -1) { return false; } _timeouts.RemoveAt (idx); } return true; } /// public object Add (TimeSpan time, Func callback) { ArgumentNullException.ThrowIfNull (callback); var timeout = new Timeout { Span = time, Callback = callback }; AddTimeout (time, timeout); return timeout; } /// public object Add (Timeout timeout) { AddTimeout (timeout.Span, timeout); return timeout; } /// public bool CheckTimers (out int waitTimeout) { long now = GetTimestampTicks (); waitTimeout = 0; lock (_timeoutsLockToken) { if (_timeouts.Count > 0) { waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); if (waitTimeout < 0) { // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0 // and no event occurred in elapsed time when the 'poll' is start running again. waitTimeout = 0; } return true; } // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if // the timeout is -1. waitTimeout = -1; } return false; } /// public TimeSpan? GetTimeout (object token) { lock (_timeoutsLockToken) { int idx = _timeouts.IndexOfValue ((token as Timeout)!); if (idx == -1) { return null; } return _timeouts.Values [idx].Span; } } private void AddTimeout (TimeSpan time, Timeout timeout) { lock (_timeoutsLockToken) { long k = GetTimestampTicks () + time.Ticks; // if user wants to run as soon as possible set timer such that it expires right away (no race conditions) if (time == TimeSpan.Zero) { // Use a more substantial buffer (1ms) to ensure it's truly in the past // even under debugger overhead and extreme timing variations k -= TimeSpan.TicksPerMillisecond; } _timeouts.Add (NudgeToUniqueKey (k), timeout); Added?.Invoke (this, new (timeout, k)); } } /// /// Finds the closest number to that is not present in /// (incrementally). /// /// /// private long NudgeToUniqueKey (long k) { lock (_timeoutsLockToken) { while (_timeouts.ContainsKey (k)) { k++; } } return k; } private void RunTimersImpl () { long now = GetTimestampTicks (); // Process due timeouts one at a time, without blocking the entire queue while (true) { Timeout? timeoutToExecute = null; long scheduledTime = 0; // Find the next due timeout lock (_timeoutsLockToken) { if (_timeouts.Count == 0) { break; // No more timeouts } // Re-evaluate current time for each iteration now = GetTimestampTicks (); // Check if the earliest timeout is due scheduledTime = _timeouts.Keys [0]; if (scheduledTime >= now) { // Earliest timeout is not yet due, we're done break; } // This timeout is due - remove it from the queue timeoutToExecute = _timeouts.Values [0]; _timeouts.RemoveAt (0); } // Execute the callback outside the lock // This allows nested Run() calls to access the timeout queue if (timeoutToExecute != null) { bool repeat = timeoutToExecute.Callback! (); if (repeat) { AddTimeout (timeoutToExecute.Span, timeoutToExecute); } } } } /// public void StopAll () { lock (_timeoutsLockToken) { _timeouts.Clear (); } } }