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