ApplicationStressTests.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. using System.Diagnostics;
  2. using Xunit.Abstractions;
  3. // ReSharper disable AccessToDisposedClosure
  4. namespace StressTests;
  5. public class ApplicationStressTests
  6. {
  7. private const int NUM_INCREMENTS = 500;
  8. private const int NUM_PASSES = 50;
  9. private const int POLL_MS_DEBUGGER = 500;
  10. private const int POLL_MS_NORMAL = 100;
  11. private static volatile int _tbCounter;
  12. #pragma warning disable IDE1006 // Naming Styles
  13. private static readonly ManualResetEventSlim _wakeUp = new (false);
  14. #pragma warning restore IDE1006 // Naming Styles
  15. /// <summary>
  16. /// Stress test for Application.Invoke to verify that invocations from background threads
  17. /// are not lost or delayed indefinitely. Tests 25,000 concurrent invocations (50 passes × 500 increments).
  18. /// </summary>
  19. /// <remarks>
  20. /// <para>
  21. /// This test automatically adapts its timeout when running under a debugger (500ms vs 100ms)
  22. /// to account for slower iteration times caused by debugger overhead.
  23. /// </para>
  24. /// <para>
  25. /// See InvokeLeakTest_Analysis.md for technical details about the timing improvements made
  26. /// to TimedEvents (Stopwatch-based timing) and Application.Invoke (MainLoop wakeup).
  27. /// </para>
  28. /// </remarks>
  29. [Fact]
  30. public async Task InvokeLeakTest ()
  31. {
  32. IApplication app = Application.Create ();
  33. app.Init ("fake");
  34. Random r = new ();
  35. TextField tf = new ();
  36. var top = new Window ();
  37. top.Add (tf);
  38. _tbCounter = 0;
  39. int pollMs = Debugger.IsAttached ? POLL_MS_DEBUGGER : POLL_MS_NORMAL;
  40. Task task = Task.Run (() => RunTest (app, r, tf, NUM_PASSES, NUM_INCREMENTS, pollMs));
  41. // blocks here until the RequestStop is processed at the end of the test
  42. app.Run (top);
  43. await task; // Propagate exception if any occurred
  44. Assert.Equal (NUM_INCREMENTS * NUM_PASSES, _tbCounter);
  45. top.Dispose ();
  46. app.Dispose ();
  47. return;
  48. void RunTest (IApplication application, Random random, TextField textField, int numPasses, int numIncrements, int pollMsValue)
  49. {
  50. for (var j = 0; j < numPasses; j++)
  51. {
  52. _wakeUp.Reset ();
  53. for (var i = 0; i < numIncrements; i++)
  54. {
  55. Launch (application, random, textField, (j + 1) * numIncrements);
  56. }
  57. int maxWaitMs = pollMsValue * 50; // Maximum total wait time (5s normal, 25s debugger)
  58. var elapsedMs = 0;
  59. while (_tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value
  60. {
  61. int tbNow = _tbCounter;
  62. // Wait for Application.TopRunnable to be running to ensure timed events can be processed
  63. var topRunnableWaitMs = 0;
  64. while (application.TopRunnableView is null or IRunnable { IsRunning: false })
  65. {
  66. Thread.Sleep (1);
  67. topRunnableWaitMs++;
  68. if (topRunnableWaitMs > maxWaitMs)
  69. {
  70. application.Invoke (application.Dispose);
  71. throw new TimeoutException (
  72. $"Timeout: TopRunnableView never started running on pass {j + 1}"
  73. );
  74. }
  75. }
  76. _wakeUp.Wait (pollMsValue);
  77. elapsedMs += pollMsValue;
  78. if (_tbCounter != tbNow)
  79. {
  80. elapsedMs = 0; // Reset elapsed time on progress
  81. continue;
  82. }
  83. if (elapsedMs > maxWaitMs)
  84. {
  85. // No change after maximum wait: Idle handlers added via Application.Invoke have gone missing
  86. application.Invoke (application.Dispose);
  87. throw new TimeoutException (
  88. $"Timeout: Increment lost. _tbCounter ({_tbCounter}) didn't "
  89. + $"change after waiting {maxWaitMs} ms (pollMs={pollMsValue}). "
  90. + $"Failed to reach {(j + 1) * numIncrements} on pass {j + 1}"
  91. );
  92. }
  93. }
  94. }
  95. application.Invoke (application.Dispose);
  96. }
  97. static void Launch (IApplication application, Random random, TextField textField, int target)
  98. {
  99. Task.Run (() =>
  100. {
  101. Thread.Sleep (random.Next (2, 4));
  102. application.Invoke (() =>
  103. {
  104. textField.Text = $"index{random.Next ()}";
  105. Interlocked.Increment (ref _tbCounter);
  106. if (target == _tbCounter)
  107. {
  108. // On last increment wake up the check
  109. _wakeUp.Set ();
  110. }
  111. }
  112. );
  113. }
  114. );
  115. }
  116. }
  117. }