TimedEvents.cs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. #nullable enable
  2. using System.Diagnostics;
  3. namespace Terminal.Gui.App;
  4. /// <summary>
  5. /// Manages scheduled timeouts (timed callbacks) for the application.
  6. /// <para>
  7. /// Allows scheduling of callbacks to be invoked after a specified delay, with optional repetition.
  8. /// Timeouts are stored in a sorted list by their scheduled execution time (high-resolution ticks).
  9. /// Thread-safe for concurrent access.
  10. /// </para>
  11. /// <para>
  12. /// Typical usage:
  13. /// <list type="number">
  14. /// <item>
  15. /// <description>Call <see cref="Add(TimeSpan, Func{bool})"/> to schedule a callback.</description>
  16. /// </item>
  17. /// <item>
  18. /// <description>
  19. /// Call <see cref="RunTimers"/> periodically (e.g., from the main loop) to execute due
  20. /// callbacks.
  21. /// </description>
  22. /// </item>
  23. /// <item>
  24. /// <description>Call <see cref="Remove"/> to cancel a scheduled timeout.</description>
  25. /// </item>
  26. /// </list>
  27. /// </para>
  28. /// </summary>
  29. /// <remarks>
  30. /// Uses <see cref="Stopwatch.GetTimestamp"/> for high-resolution timing instead of <see cref="DateTime.UtcNow"/>
  31. /// to provide microsecond-level precision and eliminate race conditions from timer resolution issues.
  32. /// </remarks>
  33. public class TimedEvents : ITimedEvents
  34. {
  35. internal SortedList<long, Timeout> _timeouts = new ();
  36. private readonly object _timeoutsLockToken = new ();
  37. /// <summary>
  38. /// Gets the list of all timeouts sorted by the <see cref="TimeSpan"/> time ticks. A shorter limit time can be
  39. /// added at the end, but it will be called before an earlier addition that has a longer limit time.
  40. /// </summary>
  41. public SortedList<long, Timeout> Timeouts => _timeouts;
  42. /// <inheritdoc/>
  43. public event EventHandler<TimeoutEventArgs>? Added;
  44. /// <summary>
  45. /// Gets the current high-resolution timestamp in TimeSpan ticks.
  46. /// Uses <see cref="Stopwatch.GetTimestamp"/> for microsecond-level precision.
  47. /// </summary>
  48. /// <returns>Current timestamp in TimeSpan ticks (100-nanosecond units).</returns>
  49. private static long GetTimestampTicks ()
  50. {
  51. // Convert Stopwatch ticks to TimeSpan ticks (100-nanosecond units)
  52. // Stopwatch.Frequency gives ticks per second, so we need to scale appropriately
  53. // To avoid overflow, we perform the operation in double precision first and then cast to long.
  54. var ticks = (long)((double)Stopwatch.GetTimestamp () * TimeSpan.TicksPerSecond / Stopwatch.Frequency);
  55. // Ensure ticks is positive and not overflowed (very unlikely now)
  56. Debug.Assert (ticks > 0);
  57. return ticks;
  58. }
  59. /// <inheritdoc/>
  60. public void RunTimers ()
  61. {
  62. lock (_timeoutsLockToken)
  63. {
  64. if (_timeouts.Count > 0)
  65. {
  66. RunTimersImpl ();
  67. }
  68. }
  69. }
  70. /// <inheritdoc/>
  71. public bool Remove (object token)
  72. {
  73. lock (_timeoutsLockToken)
  74. {
  75. int idx = _timeouts.IndexOfValue ((token as Timeout)!);
  76. if (idx == -1)
  77. {
  78. return false;
  79. }
  80. _timeouts.RemoveAt (idx);
  81. }
  82. return true;
  83. }
  84. /// <inheritdoc/>
  85. public object Add (TimeSpan time, Func<bool> callback)
  86. {
  87. ArgumentNullException.ThrowIfNull (callback);
  88. var timeout = new Timeout { Span = time, Callback = callback };
  89. AddTimeout (time, timeout);
  90. return timeout;
  91. }
  92. /// <inheritdoc/>
  93. public object Add (Timeout timeout)
  94. {
  95. AddTimeout (timeout.Span, timeout);
  96. return timeout;
  97. }
  98. /// <inheritdoc/>
  99. public bool CheckTimers (out int waitTimeout)
  100. {
  101. long now = GetTimestampTicks ();
  102. waitTimeout = 0;
  103. lock (_timeoutsLockToken)
  104. {
  105. if (_timeouts.Count > 0)
  106. {
  107. waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
  108. if (waitTimeout < 0)
  109. {
  110. // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected
  111. // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0
  112. // and no event occurred in elapsed time when the 'poll' is start running again.
  113. waitTimeout = 0;
  114. }
  115. return true;
  116. }
  117. // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if
  118. // the timeout is -1.
  119. waitTimeout = -1;
  120. }
  121. return false;
  122. }
  123. private void AddTimeout (TimeSpan time, Timeout timeout)
  124. {
  125. lock (_timeoutsLockToken)
  126. {
  127. long k = GetTimestampTicks () + time.Ticks;
  128. // if user wants to run as soon as possible set timer such that it expires right away (no race conditions)
  129. if (time == TimeSpan.Zero)
  130. {
  131. // Use a more substantial buffer (1ms) to ensure it's truly in the past
  132. // even under debugger overhead and extreme timing variations
  133. k -= TimeSpan.TicksPerMillisecond;
  134. }
  135. _timeouts.Add (NudgeToUniqueKey (k), timeout);
  136. Added?.Invoke (this, new (timeout, k));
  137. }
  138. }
  139. /// <summary>
  140. /// Finds the closest number to <paramref name="k"/> that is not present in <see cref="_timeouts"/>
  141. /// (incrementally).
  142. /// </summary>
  143. /// <param name="k"></param>
  144. /// <returns></returns>
  145. private long NudgeToUniqueKey (long k)
  146. {
  147. lock (_timeoutsLockToken)
  148. {
  149. while (_timeouts.ContainsKey (k))
  150. {
  151. k++;
  152. }
  153. }
  154. return k;
  155. }
  156. private void RunTimersImpl ()
  157. {
  158. long now = GetTimestampTicks ();
  159. SortedList<long, Timeout> copy;
  160. // lock prevents new timeouts being added
  161. // after we have taken the copy but before
  162. // we have allocated a new list (which would
  163. // result in lost timeouts or errors during enumeration)
  164. lock (_timeoutsLockToken)
  165. {
  166. copy = _timeouts;
  167. _timeouts = new ();
  168. }
  169. foreach ((long k, Timeout timeout) in copy)
  170. {
  171. if (k < now)
  172. {
  173. if (timeout.Callback ())
  174. {
  175. AddTimeout (timeout.Span, timeout);
  176. }
  177. }
  178. else
  179. {
  180. lock (_timeoutsLockToken)
  181. {
  182. _timeouts.Add (NudgeToUniqueKey (k), timeout);
  183. }
  184. }
  185. }
  186. }
  187. }