| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- using System.Diagnostics;
- using Xunit.Abstractions;
- // ReSharper disable AccessToDisposedClosure
- namespace StressTests;
- public class ApplicationStressTests
- {
- private const int NUM_INCREMENTS = 500;
- private const int NUM_PASSES = 50;
- private const int POLL_MS_DEBUGGER = 500;
- private const int POLL_MS_NORMAL = 100;
- private static volatile int _tbCounter;
- #pragma warning disable IDE1006 // Naming Styles
- private static readonly ManualResetEventSlim _wakeUp = new (false);
- #pragma warning restore IDE1006 // Naming Styles
- /// <summary>
- /// Stress test for Application.Invoke to verify that invocations from background threads
- /// are not lost or delayed indefinitely. Tests 25,000 concurrent invocations (50 passes × 500 increments).
- /// </summary>
- /// <remarks>
- /// <para>
- /// This test automatically adapts its timeout when running under a debugger (500ms vs 100ms)
- /// to account for slower iteration times caused by debugger overhead.
- /// </para>
- /// <para>
- /// See InvokeLeakTest_Analysis.md for technical details about the timing improvements made
- /// to TimedEvents (Stopwatch-based timing) and Application.Invoke (MainLoop wakeup).
- /// </para>
- /// </remarks>
- [Fact]
- public async Task InvokeLeakTest ()
- {
- IApplication app = Application.Create ();
- app.Init ("fake");
- Random r = new ();
- TextField tf = new ();
- var top = new Window ();
- top.Add (tf);
- _tbCounter = 0;
- int pollMs = Debugger.IsAttached ? POLL_MS_DEBUGGER : POLL_MS_NORMAL;
- Task task = Task.Run (() => RunTest (app, r, tf, NUM_PASSES, NUM_INCREMENTS, pollMs));
- // blocks here until the RequestStop is processed at the end of the test
- app.Run (top);
- await task; // Propagate exception if any occurred
- Assert.Equal (NUM_INCREMENTS * NUM_PASSES, _tbCounter);
- top.Dispose ();
- app.Dispose ();
- return;
- void RunTest (IApplication application, Random random, TextField textField, int numPasses, int numIncrements, int pollMsValue)
- {
- for (var j = 0; j < numPasses; j++)
- {
- _wakeUp.Reset ();
- for (var i = 0; i < numIncrements; i++)
- {
- Launch (application, random, textField, (j + 1) * numIncrements);
- }
- int maxWaitMs = pollMsValue * 50; // Maximum total wait time (5s normal, 25s debugger)
- var elapsedMs = 0;
- while (_tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value
- {
- int tbNow = _tbCounter;
- // Wait for Application.TopRunnable to be running to ensure timed events can be processed
- var topRunnableWaitMs = 0;
- while (application.TopRunnableView is null or IRunnable { IsRunning: false })
- {
- Thread.Sleep (1);
- topRunnableWaitMs++;
- if (topRunnableWaitMs > maxWaitMs)
- {
- application.Invoke (application.Dispose);
- throw new TimeoutException (
- $"Timeout: TopRunnableView never started running on pass {j + 1}"
- );
- }
- }
- _wakeUp.Wait (pollMsValue);
- elapsedMs += pollMsValue;
- if (_tbCounter != tbNow)
- {
- elapsedMs = 0; // Reset elapsed time on progress
- continue;
- }
- if (elapsedMs > maxWaitMs)
- {
- // No change after maximum wait: Idle handlers added via Application.Invoke have gone missing
- application.Invoke (application.Dispose);
- throw new TimeoutException (
- $"Timeout: Increment lost. _tbCounter ({_tbCounter}) didn't "
- + $"change after waiting {maxWaitMs} ms (pollMs={pollMsValue}). "
- + $"Failed to reach {(j + 1) * numIncrements} on pass {j + 1}"
- );
- }
- }
- }
- application.Invoke (application.Dispose);
- }
- static void Launch (IApplication application, Random random, TextField textField, int target)
- {
- Task.Run (() =>
- {
- Thread.Sleep (random.Next (2, 4));
- application.Invoke (() =>
- {
- textField.Text = $"index{random.Next ()}";
- Interlocked.Increment (ref _tbCounter);
- if (target == _tbCounter)
- {
- // On last increment wake up the check
- _wakeUp.Set ();
- }
- }
- );
- }
- );
- }
- }
- }
|