ApplicationStressTests.cs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. using Xunit.Abstractions;
  2. namespace StressTests;
  3. public class ApplicationStressTests
  4. {
  5. public ApplicationStressTests (ITestOutputHelper output)
  6. {
  7. }
  8. private static volatile int _tbCounter;
  9. #pragma warning disable IDE1006 // Naming Styles
  10. private static readonly ManualResetEventSlim _wakeUp = new (false);
  11. #pragma warning restore IDE1006 // Naming Styles
  12. private const int NUM_PASSES = 50;
  13. private const int NUM_INCREMENTS = 500;
  14. private const int POLL_MS = 100;
  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. Application.Init (driverName: "fake");
  33. Random r = new ();
  34. TextField tf = new ();
  35. var top = new Toplevel ();
  36. top.Add (tf);
  37. _tbCounter = 0;
  38. Task task = Task.Run (() => RunTest (r, tf, NUM_PASSES, NUM_INCREMENTS, POLL_MS));
  39. // blocks here until the RequestStop is processed at the end of the test
  40. Application.Run (top);
  41. await task; // Propagate exception if any occurred
  42. Assert.Equal (NUM_INCREMENTS * NUM_PASSES, _tbCounter);
  43. top.Dispose ();
  44. Application.Shutdown ();
  45. return;
  46. static void RunTest (Random r, TextField tf, int numPasses, int numIncrements, int pollMs)
  47. {
  48. for (var j = 0; j < numPasses; j++)
  49. {
  50. _wakeUp.Reset ();
  51. for (var i = 0; i < numIncrements; i++)
  52. {
  53. Launch (r, tf, (j + 1) * numIncrements);
  54. }
  55. while (_tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value
  56. {
  57. int tbNow = _tbCounter;
  58. // Wait for Application.Current to be running to ensure timed events can be processed
  59. while (Application.Current is null || Application.Current is { Running: false })
  60. {
  61. Thread.Sleep (1);
  62. }
  63. _wakeUp.Wait (pollMs);
  64. if (_tbCounter != tbNow)
  65. {
  66. continue;
  67. }
  68. // No change after wait: Idle handlers added via Application.Invoke have gone missing
  69. Application.Invoke (() => Application.RequestStop ());
  70. throw new TimeoutException (
  71. $"Timeout: Increment lost. _tbCounter ({_tbCounter}) didn't "
  72. + $"change after waiting {pollMs} ms. Failed to reach {(j + 1) * numIncrements} on pass {j + 1}"
  73. );
  74. }
  75. ;
  76. }
  77. Application.Invoke (() => Application.RequestStop ());
  78. }
  79. static void Launch (Random r, TextField tf, int target)
  80. {
  81. Task.Run (
  82. () =>
  83. {
  84. Thread.Sleep (r.Next (2, 4));
  85. Application.Invoke (
  86. () =>
  87. {
  88. tf.Text = $"index{r.Next ()}";
  89. Interlocked.Increment (ref _tbCounter);
  90. if (target == _tbCounter)
  91. {
  92. // On last increment wake up the check
  93. _wakeUp.Set ();
  94. }
  95. }
  96. );
  97. }
  98. );
  99. }
  100. }
  101. }