TimedEvents.cs 7.2 KB

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