AnsiRequestScheduler.cs 5.5 KB

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