AnsiRequestScheduler.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. namespace Terminal.Gui;
  4. public class AnsiRequestScheduler (IAnsiResponseParser parser)
  5. {
  6. private readonly List<Tuple<AnsiEscapeSequenceRequest, DateTime>> _requests = new ();
  7. /// <summary>
  8. ///<para>
  9. /// Dictionary where key is ansi request terminator and value is when we last sent a request for
  10. /// this terminator. Combined with <see cref="_throttle"/> this prevents hammering the console
  11. /// with too many requests in sequence which can cause console to freeze as there is no space for
  12. /// regular screen drawing / mouse events etc to come in.
  13. /// </para>
  14. /// <para>
  15. /// When user exceeds the throttle, new requests accumulate in <see cref="_requests"/> (i.e. remain
  16. /// queued).
  17. /// </para>
  18. /// </summary>
  19. private ConcurrentDictionary<string, DateTime> _lastSend = new ();
  20. private TimeSpan _throttle = TimeSpan.FromMilliseconds (100);
  21. private TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100);
  22. /// <summary>
  23. /// If console has not responded to a request after this period of time, we assume that it is never going
  24. /// to respond. Only affects when we try to send a new request with the same terminator - at which point
  25. /// we tell the parser to stop expecting the old request and start expecting the new request.
  26. /// </summary>
  27. private TimeSpan _staleTimeout = TimeSpan.FromSeconds (5);
  28. /// <summary>
  29. /// Sends the <paramref name="request"/> immediately or queues it if there is already
  30. /// an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>.
  31. /// </summary>
  32. /// <param name="request"></param>
  33. /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
  34. public bool SendOrSchedule (AnsiEscapeSequenceRequest request)
  35. {
  36. if (CanSend (request, out var reason))
  37. {
  38. Send (request);
  39. return true;
  40. }
  41. if (reason == ReasonCannotSend.OutstandingRequest)
  42. {
  43. EvictStaleRequests (request.Terminator);
  44. // Try again after
  45. if (CanSend (request, out _))
  46. {
  47. Send (request);
  48. return true;
  49. }
  50. }
  51. _requests.Add (Tuple.Create (request, DateTime.Now));
  52. return false;
  53. }
  54. /// <summary>
  55. /// Looks to see if the last time we sent <paramref name="withTerminator"/>
  56. /// is a long time ago. If so we assume that we will never get a response and
  57. /// can proceed with a new request for this terminator (returning <see langword="true"/>).
  58. /// </summary>
  59. /// <param name="withTerminator"></param>
  60. /// <returns></returns>
  61. private bool EvictStaleRequests (string withTerminator)
  62. {
  63. if (_lastSend.TryGetValue (withTerminator, out var dt))
  64. {
  65. if (DateTime.Now - dt > _staleTimeout)
  66. {
  67. parser.StopExpecting (withTerminator, false);
  68. return true;
  69. }
  70. }
  71. return false;
  72. }
  73. private DateTime _lastRun = DateTime.Now;
  74. /// <summary>
  75. /// Identifies and runs any <see cref="_requests"/> that can be sent based on the
  76. /// current outstanding requests of the parser.
  77. /// </summary>
  78. /// <param name="force">Repeated requests to run the schedule over short period of time will be ignored.
  79. /// Pass <see langword="true"/> to override this behaviour and force evaluation of outstanding requests.</param>
  80. /// <returns><see langword="true"/> if a request was found and run. <see langword="false"/>
  81. /// if no outstanding requests or all have existing outstanding requests underway in parser.</returns>
  82. public bool RunSchedule (bool force = false)
  83. {
  84. if (!force && DateTime.Now - _lastRun < _runScheduleThrottle)
  85. {
  86. return false;
  87. }
  88. var opportunity = _requests.FirstOrDefault (r => CanSend (r.Item1, out _));
  89. if (opportunity != null)
  90. {
  91. _requests.Remove (opportunity);
  92. Send (opportunity.Item1);
  93. return true;
  94. }
  95. return false;
  96. }
  97. private void Send (AnsiEscapeSequenceRequest r)
  98. {
  99. _lastSend.AddOrUpdate (r.Terminator, (s) => DateTime.Now, (s, v) => DateTime.Now);
  100. parser.ExpectResponse (r.Terminator, r.ResponseReceived, false);
  101. r.Send ();
  102. }
  103. private bool CanSend (AnsiEscapeSequenceRequest r, out ReasonCannotSend reason)
  104. {
  105. if (ShouldThrottle (r))
  106. {
  107. reason = ReasonCannotSend.TooManyRequests;
  108. return false;
  109. }
  110. if (parser.IsExpecting (r.Terminator))
  111. {
  112. reason = ReasonCannotSend.OutstandingRequest;
  113. return false;
  114. }
  115. reason = default;
  116. return true;
  117. }
  118. private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
  119. {
  120. if (_lastSend.TryGetValue (r.Terminator, out DateTime value))
  121. {
  122. return DateTime.Now - value < _throttle;
  123. }
  124. return false;
  125. }
  126. }
  127. internal enum ReasonCannotSend
  128. {
  129. /// <summary>
  130. /// No reason given.
  131. /// </summary>
  132. None = 0,
  133. /// <summary>
  134. /// The parser is already waiting for a request to complete with the given terminator.
  135. /// </summary>
  136. OutstandingRequest,
  137. /// <summary>
  138. /// There have been too many requests sent recently, new requests will be put into
  139. /// queue to prevent console becoming unresponsive.
  140. /// </summary>
  141. TooManyRequests
  142. }