TimedEvents.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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. return Stopwatch.GetTimestamp () * TimeSpan.TicksPerSecond / Stopwatch.Frequency;
  54. }
  55. /// <inheritdoc/>
  56. public void RunTimers ()
  57. {
  58. lock (_timeoutsLockToken)
  59. {
  60. if (_timeouts.Count > 0)
  61. {
  62. RunTimersImpl ();
  63. }
  64. }
  65. }
  66. /// <inheritdoc/>
  67. public bool Remove (object token)
  68. {
  69. lock (_timeoutsLockToken)
  70. {
  71. int idx = _timeouts.IndexOfValue ((token as Timeout)!);
  72. if (idx == -1)
  73. {
  74. return false;
  75. }
  76. _timeouts.RemoveAt (idx);
  77. }
  78. return true;
  79. }
  80. /// <inheritdoc/>
  81. public object Add (TimeSpan time, Func<bool> callback)
  82. {
  83. ArgumentNullException.ThrowIfNull (callback);
  84. var timeout = new Timeout { Span = time, Callback = callback };
  85. AddTimeout (time, timeout);
  86. return timeout;
  87. }
  88. /// <inheritdoc/>
  89. public object Add (Timeout timeout)
  90. {
  91. AddTimeout (timeout.Span, timeout);
  92. return timeout;
  93. }
  94. /// <inheritdoc/>
  95. public bool CheckTimers (out int waitTimeout)
  96. {
  97. long now = GetTimestampTicks ();
  98. waitTimeout = 0;
  99. lock (_timeoutsLockToken)
  100. {
  101. if (_timeouts.Count > 0)
  102. {
  103. waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
  104. if (waitTimeout < 0)
  105. {
  106. // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected
  107. // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0
  108. // and no event occurred in elapsed time when the 'poll' is start running again.
  109. waitTimeout = 0;
  110. }
  111. return true;
  112. }
  113. // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if
  114. // the timeout is -1.
  115. waitTimeout = -1;
  116. }
  117. return false;
  118. }
  119. private void AddTimeout (TimeSpan time, Timeout timeout)
  120. {
  121. lock (_timeoutsLockToken)
  122. {
  123. long k = GetTimestampTicks () + time.Ticks;
  124. // if user wants to run as soon as possible set timer such that it expires right away (no race conditions)
  125. if (time == TimeSpan.Zero)
  126. {
  127. // Use a more substantial buffer (1ms) to ensure it's truly in the past
  128. // even under debugger overhead and extreme timing variations
  129. k -= TimeSpan.TicksPerMillisecond;
  130. }
  131. _timeouts.Add (NudgeToUniqueKey (k), timeout);
  132. Added?.Invoke (this, new (timeout, k));
  133. }
  134. }
  135. /// <summary>
  136. /// Finds the closest number to <paramref name="k"/> that is not present in <see cref="_timeouts"/>
  137. /// (incrementally).
  138. /// </summary>
  139. /// <param name="k"></param>
  140. /// <returns></returns>
  141. private long NudgeToUniqueKey (long k)
  142. {
  143. lock (_timeoutsLockToken)
  144. {
  145. while (_timeouts.ContainsKey (k))
  146. {
  147. k++;
  148. }
  149. }
  150. return k;
  151. }
  152. private void RunTimersImpl ()
  153. {
  154. long now = GetTimestampTicks ();
  155. SortedList<long, Timeout> copy;
  156. // lock prevents new timeouts being added
  157. // after we have taken the copy but before
  158. // we have allocated a new list (which would
  159. // result in lost timeouts or errors during enumeration)
  160. lock (_timeoutsLockToken)
  161. {
  162. copy = _timeouts;
  163. _timeouts = new ();
  164. }
  165. foreach ((long k, Timeout timeout) in copy)
  166. {
  167. if (k < now)
  168. {
  169. if (timeout.Callback ())
  170. {
  171. AddTimeout (timeout.Span, timeout);
  172. }
  173. }
  174. else
  175. {
  176. lock (_timeoutsLockToken)
  177. {
  178. _timeouts.Add (NudgeToUniqueKey (k), timeout);
  179. }
  180. }
  181. }
  182. }
  183. }