AnsiRequestScheduler.cs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. namespace Terminal.Gui;
  4. public class AnsiRequestScheduler(IAnsiResponseParser parser)
  5. {
  6. private readonly List<AnsiEscapeSequenceRequest> _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. /// Sends the <paramref name="request"/> immediately or queues it if there is already
  24. /// an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>.
  25. /// </summary>
  26. /// <param name="request"></param>
  27. /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
  28. public bool SendOrSchedule (AnsiEscapeSequenceRequest request )
  29. {
  30. if (CanSend(request))
  31. {
  32. Send (request);
  33. return true;
  34. }
  35. else
  36. {
  37. _requests.Add (request);
  38. return false;
  39. }
  40. }
  41. private DateTime _lastRun = DateTime.Now;
  42. /// <summary>
  43. /// Identifies and runs any <see cref="_requests"/> that can be sent based on the
  44. /// current outstanding requests of the parser.
  45. /// </summary>
  46. /// <param name="force">Repeated requests to run the schedule over short period of time will be ignored.
  47. /// Pass <see langword="true"/> to override this behaviour and force evaluation of outstanding requests.</param>
  48. /// <returns><see langword="true"/> if a request was found and run. <see langword="false"/>
  49. /// if no outstanding requests or all have existing outstanding requests underway in parser.</returns>
  50. public bool RunSchedule (bool force = false)
  51. {
  52. if (!force && DateTime.Now - _lastRun < _runScheduleThrottle)
  53. {
  54. return false;
  55. }
  56. var opportunity = _requests.FirstOrDefault (CanSend);
  57. if (opportunity != null)
  58. {
  59. _requests.Remove (opportunity);
  60. Send (opportunity);
  61. return true;
  62. }
  63. return false;
  64. }
  65. private void Send (AnsiEscapeSequenceRequest r)
  66. {
  67. _lastSend.AddOrUpdate (r.Terminator,(s)=>DateTime.Now,(s,v)=>DateTime.Now);
  68. parser.ExpectResponse (r.Terminator,r.ResponseReceived);
  69. r.Send ();
  70. }
  71. public bool CanSend (AnsiEscapeSequenceRequest r)
  72. {
  73. if (ShouldThrottle (r))
  74. {
  75. return false;
  76. }
  77. return !parser.IsExpecting (r.Terminator);
  78. }
  79. private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
  80. {
  81. if (_lastSend.TryGetValue (r.Terminator, out DateTime value))
  82. {
  83. return DateTime.Now - value < _throttle;
  84. }
  85. return false;
  86. }
  87. }