DispatcherTests.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using Avalonia.Controls.Platform;
  9. using Avalonia.Threading;
  10. using Avalonia.Utilities;
  11. using Xunit;
  12. namespace Avalonia.Base.UnitTests;
  13. public partial class DispatcherTests
  14. {
  15. class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherImplWithPendingInput
  16. {
  17. private readonly Thread _loopThread = Thread.CurrentThread;
  18. private readonly object _lock = new();
  19. public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _loopThread;
  20. public void Signal()
  21. {
  22. lock (_lock)
  23. AskedForSignal = true;
  24. }
  25. public event Action Signaled;
  26. public event Action Timer;
  27. public long? NextTimer { get; private set; }
  28. public bool AskedForSignal { get; private set; }
  29. public void UpdateTimer(long? dueTimeInTicks)
  30. {
  31. NextTimer = dueTimeInTicks;
  32. }
  33. public long Now { get; set; }
  34. public void ExecuteSignal()
  35. {
  36. lock (_lock)
  37. {
  38. if (!AskedForSignal)
  39. return;
  40. AskedForSignal = false;
  41. }
  42. Signaled?.Invoke();
  43. }
  44. public void ExecuteTimer()
  45. {
  46. if (NextTimer == null)
  47. return;
  48. Now = NextTimer.Value;
  49. Timer?.Invoke();
  50. }
  51. public bool CanQueryPendingInput => TestInputPending != null;
  52. public bool HasPendingInput => TestInputPending == true;
  53. public bool? TestInputPending { get; set; }
  54. }
  55. class SimpleDispatcherWithBackgroundProcessingImpl : SimpleDispatcherImpl, IDispatcherImplWithExplicitBackgroundProcessing
  56. {
  57. public bool AskedForBackgroundProcessing { get; private set; }
  58. public event Action ReadyForBackgroundProcessing;
  59. public void RequestBackgroundProcessing()
  60. {
  61. if (!CurrentThreadIsLoopThread)
  62. throw new InvalidOperationException();
  63. AskedForBackgroundProcessing = true;
  64. }
  65. public void FireBackgroundProcessing()
  66. {
  67. if(!AskedForBackgroundProcessing)
  68. return;
  69. AskedForBackgroundProcessing = false;
  70. ReadyForBackgroundProcessing?.Invoke();
  71. }
  72. }
  73. class SimpleControlledDispatcherImpl : SimpleDispatcherWithBackgroundProcessingImpl, IControlledDispatcherImpl
  74. {
  75. private readonly bool _useTestTimeout = true;
  76. private readonly CancellationToken? _cancel;
  77. public int RunLoopCount { get; private set; }
  78. public SimpleControlledDispatcherImpl()
  79. {
  80. }
  81. public SimpleControlledDispatcherImpl(CancellationToken cancel, bool useTestTimeout = false)
  82. {
  83. _useTestTimeout = useTestTimeout;
  84. _cancel = cancel;
  85. }
  86. public void RunLoop(CancellationToken token)
  87. {
  88. RunLoopCount++;
  89. var st = Stopwatch.StartNew();
  90. while (!token.IsCancellationRequested || _cancel?.IsCancellationRequested == true)
  91. {
  92. FireBackgroundProcessing();
  93. ExecuteSignal();
  94. if (_useTestTimeout)
  95. Assert.True(st.ElapsedMilliseconds < 4000, "RunLoop exceeded test time quota");
  96. else
  97. Thread.Sleep(10);
  98. }
  99. }
  100. }
  101. [Fact]
  102. public void DispatcherExecutesJobsAccordingToPriority()
  103. {
  104. var impl = new SimpleDispatcherImpl();
  105. var disp = new Dispatcher(impl);
  106. var actions = new List<string>();
  107. disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background);
  108. disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render);
  109. disp.Post(()=>actions.Add("Input"), DispatcherPriority.Input);
  110. Assert.True(impl.AskedForSignal);
  111. impl.ExecuteSignal();
  112. Assert.Equal(new[] { "Render", "Input", "Background" }, actions);
  113. }
  114. [Fact]
  115. public void DispatcherPreservesOrderWhenChangingPriority()
  116. {
  117. var impl = new SimpleDispatcherImpl();
  118. var disp = new Dispatcher(impl);
  119. var actions = new List<string>();
  120. var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background);
  121. var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input);
  122. disp.Post(() => actions.Add("Render"), DispatcherPriority.Render);
  123. toPromote.Priority = DispatcherPriority.Render;
  124. toPromote2.Priority = DispatcherPriority.Render;
  125. Assert.True(impl.AskedForSignal);
  126. impl.ExecuteSignal();
  127. Assert.Equal(new[] { "PromotedRender", "PromotedRender2", "Render" }, actions);
  128. }
  129. [Fact]
  130. public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached()
  131. {
  132. var impl = new SimpleDispatcherImpl();
  133. var disp = new Dispatcher(impl);
  134. var actions = new List<int>();
  135. for (var c = 0; c < 10; c++)
  136. {
  137. var itemId = c;
  138. disp.Post(() =>
  139. {
  140. actions.Add(itemId);
  141. impl.Now += 20;
  142. }, DispatcherPriority.Background);
  143. }
  144. Assert.False(impl.AskedForSignal);
  145. Assert.NotNull(impl.NextTimer);
  146. for (var c = 0; c < 4; c++)
  147. {
  148. Assert.NotNull(impl.NextTimer);
  149. Assert.False(impl.AskedForSignal);
  150. impl.ExecuteTimer();
  151. Assert.False(impl.AskedForSignal);
  152. impl.ExecuteSignal();
  153. var expectedCount = (c + 1) * 3;
  154. if (c == 3)
  155. expectedCount = 10;
  156. Assert.Equal(Enumerable.Range(0, expectedCount), actions);
  157. Assert.False(impl.AskedForSignal);
  158. if (c < 3)
  159. {
  160. Assert.True(impl.NextTimer > impl.Now);
  161. }
  162. else
  163. Assert.Null(impl.NextTimer);
  164. }
  165. }
  166. [Fact]
  167. public void DispatcherStopsItemProcessingWhenInputIsPending()
  168. {
  169. var impl = new SimpleDispatcherImpl();
  170. impl.TestInputPending = true;
  171. var disp = new Dispatcher(impl);
  172. var actions = new List<int>();
  173. for (var c = 0; c < 10; c++)
  174. {
  175. var itemId = c;
  176. disp.Post(() =>
  177. {
  178. actions.Add(itemId);
  179. if (itemId == 0 || itemId == 3 || itemId == 7)
  180. impl.TestInputPending = true;
  181. }, DispatcherPriority.Background);
  182. }
  183. Assert.False(impl.AskedForSignal);
  184. Assert.NotNull(impl.NextTimer);
  185. impl.TestInputPending = false;
  186. for (var c = 0; c < 4; c++)
  187. {
  188. Assert.NotNull(impl.NextTimer);
  189. impl.ExecuteTimer();
  190. Assert.False(impl.AskedForSignal);
  191. var expectedCount = c switch
  192. {
  193. 0 => 1,
  194. 1 => 4,
  195. 2 => 8,
  196. 3 => 10,
  197. _ => throw new InvalidOperationException($"Unexpected value {c}")
  198. };
  199. Assert.Equal(Enumerable.Range(0, expectedCount), actions);
  200. Assert.False(impl.AskedForSignal);
  201. if (c < 3)
  202. {
  203. Assert.True(impl.NextTimer > impl.Now);
  204. impl.Now = impl.NextTimer.Value + 1;
  205. }
  206. else
  207. Assert.Null(impl.NextTimer);
  208. impl.TestInputPending = false;
  209. }
  210. }
  211. [Theory,
  212. InlineData(false, false),
  213. InlineData(false, true),
  214. InlineData(true, false),
  215. InlineData(true, true)]
  216. public void CanWaitForDispatcherOperationFromTheSameThread(bool controlled, bool foreground)
  217. {
  218. var impl = controlled ? new SimpleControlledDispatcherImpl() : new SimpleDispatcherImpl();
  219. var disp = new Dispatcher(impl);
  220. bool finished = false;
  221. disp.InvokeAsync(() => finished = true,
  222. foreground ? DispatcherPriority.Default : DispatcherPriority.Background).Wait();
  223. Assert.True(finished);
  224. if (controlled)
  225. Assert.Equal(foreground ? 0 : 1, ((SimpleControlledDispatcherImpl)impl).RunLoopCount);
  226. }
  227. class DispatcherServices : IDisposable
  228. {
  229. private readonly IDisposable _scope;
  230. public DispatcherServices(IDispatcherImpl impl)
  231. {
  232. _scope = AvaloniaLocator.EnterScope();
  233. AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToConstant(impl);
  234. Dispatcher.ResetForUnitTests();
  235. SynchronizationContext.SetSynchronizationContext(null);
  236. }
  237. public void Dispose()
  238. {
  239. Dispatcher.ResetForUnitTests();
  240. _scope.Dispose();
  241. SynchronizationContext.SetSynchronizationContext(null);
  242. }
  243. }
  244. [Fact]
  245. public void ExitAllFramesShouldExitAllFramesAndBeAbleToContinue()
  246. {
  247. using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
  248. {
  249. var actions = new List<string>();
  250. var disp = Dispatcher.UIThread;
  251. disp.Post(() =>
  252. {
  253. actions.Add("Nested frame");
  254. Dispatcher.UIThread.MainLoop(CancellationToken.None);
  255. actions.Add("Nested frame exited");
  256. });
  257. disp.Post(() =>
  258. {
  259. actions.Add("ExitAllFrames");
  260. disp.ExitAllFrames();
  261. });
  262. disp.MainLoop(CancellationToken.None);
  263. Assert.Equal(new[] { "Nested frame", "ExitAllFrames", "Nested frame exited" }, actions);
  264. actions.Clear();
  265. var secondLoop = new CancellationTokenSource();
  266. disp.Post(() =>
  267. {
  268. actions.Add("Callback after exit");
  269. secondLoop.Cancel();
  270. });
  271. disp.MainLoop(secondLoop.Token);
  272. Assert.Equal(new[] { "Callback after exit" }, actions);
  273. }
  274. }
  275. [Fact]
  276. public void ShutdownShouldExitAllFramesAndNotAllowNewFrames()
  277. {
  278. using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
  279. {
  280. var actions = new List<string>();
  281. var disp = Dispatcher.UIThread;
  282. disp.Post(() =>
  283. {
  284. actions.Add("Nested frame");
  285. Dispatcher.UIThread.MainLoop(CancellationToken.None);
  286. actions.Add("Nested frame exited");
  287. });
  288. disp.Post(() =>
  289. {
  290. actions.Add("Shutdown");
  291. disp.BeginInvokeShutdown(DispatcherPriority.Normal);
  292. });
  293. disp.Post(() =>
  294. {
  295. actions.Add("Nested frame after shutdown");
  296. // This should exit immediately and not run any jobs
  297. Dispatcher.UIThread.MainLoop(CancellationToken.None);
  298. actions.Add("Nested frame after shutdown exited");
  299. });
  300. var criticalFrameAfterShutdown = new DispatcherFrame(false);
  301. disp.Post(() =>
  302. {
  303. actions.Add("Critical frame after shutdown");
  304. Dispatcher.UIThread.PushFrame(criticalFrameAfterShutdown);
  305. actions.Add("Critical frame after shutdown exited");
  306. });
  307. disp.Post(() =>
  308. {
  309. actions.Add("Stop critical frame");
  310. criticalFrameAfterShutdown.Continue = false;
  311. });
  312. disp.MainLoop(CancellationToken.None);
  313. Assert.Equal(new[]
  314. {
  315. "Nested frame",
  316. "Shutdown",
  317. // Normal nested frames are supposed to exit immediately
  318. "Nested frame after shutdown", "Nested frame after shutdown exited",
  319. // if frame is configured to not answer dispatcher requests, it should be allowed to run
  320. "Critical frame after shutdown", "Stop critical frame", "Critical frame after shutdown exited",
  321. // After 3-rd level frames have exited, the normal nested frame exits too
  322. "Nested frame exited"
  323. }, actions);
  324. actions.Clear();
  325. disp.Post(()=>actions.Add("Frame after shutdown finished"));
  326. Assert.Throws<InvalidOperationException>(() => disp.MainLoop(CancellationToken.None));
  327. Assert.Empty(actions);
  328. }
  329. }
  330. class WaitHelper : SynchronizationContext, NonPumpingLockHelper.IHelperImpl
  331. {
  332. public int WaitCount;
  333. public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
  334. {
  335. WaitCount++;
  336. return base.Wait(waitHandles, waitAll, millisecondsTimeout);
  337. }
  338. }
  339. [Fact]
  340. public void DisableProcessingShouldStopProcessing()
  341. {
  342. using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
  343. {
  344. var helper = new WaitHelper();
  345. AvaloniaLocator.CurrentMutable.Bind<NonPumpingLockHelper.IHelperImpl>().ToConstant(helper);
  346. using (Dispatcher.UIThread.DisableProcessing())
  347. {
  348. Assert.True(SynchronizationContext.Current is NonPumpingSyncContext);
  349. Assert.Throws<InvalidOperationException>(() => Dispatcher.UIThread.MainLoop(CancellationToken.None));
  350. Assert.Throws<InvalidOperationException>(() => Dispatcher.UIThread.RunJobs());
  351. }
  352. var avaloniaContext = new AvaloniaSynchronizationContext(Dispatcher.UIThread, DispatcherPriority.Default, true);
  353. SynchronizationContext.SetSynchronizationContext(avaloniaContext);
  354. var waitHandle = new ManualResetEvent(true);
  355. helper.WaitCount = 0;
  356. waitHandle.WaitOne(100);
  357. Assert.Equal(0, helper.WaitCount);
  358. using (Dispatcher.UIThread.DisableProcessing())
  359. {
  360. Assert.Equal(avaloniaContext, SynchronizationContext.Current);
  361. waitHandle.WaitOne(100);
  362. Assert.Equal(1, helper.WaitCount);
  363. }
  364. }
  365. }
  366. [Fact]
  367. public void DispatcherOperationsHaveContextWithProperPriority()
  368. {
  369. using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
  370. {
  371. SynchronizationContext.SetSynchronizationContext(null);
  372. var disp = Dispatcher.UIThread;
  373. var priorities = new List<DispatcherPriority>();
  374. void DumpCurrentPriority() =>
  375. priorities.Add(((AvaloniaSynchronizationContext)SynchronizationContext.Current!).Priority);
  376. disp.Post(DumpCurrentPriority, DispatcherPriority.Normal);
  377. disp.Post(DumpCurrentPriority, DispatcherPriority.Loaded);
  378. disp.Post(DumpCurrentPriority, DispatcherPriority.Input);
  379. disp.Post(() =>
  380. {
  381. DumpCurrentPriority();
  382. disp.ExitAllFrames();
  383. }, DispatcherPriority.Background);
  384. disp.MainLoop(CancellationToken.None);
  385. disp.Invoke(DumpCurrentPriority, DispatcherPriority.Send);
  386. disp.Invoke(() =>
  387. {
  388. DumpCurrentPriority();
  389. return 1;
  390. }, DispatcherPriority.Send);
  391. Assert.Equal(
  392. new[]
  393. {
  394. DispatcherPriority.Normal, DispatcherPriority.Loaded, DispatcherPriority.Input, DispatcherPriority.Background,
  395. DispatcherPriority.Send, DispatcherPriority.Send,
  396. },
  397. priorities);
  398. }
  399. }
  400. [Fact]
  401. [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method", Justification = "Tests the dispatcher itself")]
  402. public void DispatcherInvokeAsyncUnwrapsTasks()
  403. {
  404. int asyncMethodStage = 0;
  405. async Task AsyncMethod()
  406. {
  407. asyncMethodStage = 1;
  408. await Task.Delay(200);
  409. asyncMethodStage = 2;
  410. }
  411. async Task<int> AsyncMethodWithResult()
  412. {
  413. await Task.Delay(100);
  414. return 1;
  415. }
  416. async Task Test()
  417. {
  418. await Dispatcher.UIThread.InvokeAsync(AsyncMethod);
  419. Assert.Equal(2, asyncMethodStage);
  420. Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult));
  421. asyncMethodStage = 0;
  422. await Dispatcher.UIThread.InvokeAsync(AsyncMethod, DispatcherPriority.Default);
  423. Assert.Equal(2, asyncMethodStage);
  424. Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult, DispatcherPriority.Default));
  425. Dispatcher.UIThread.ExitAllFrames();
  426. }
  427. using (new DispatcherServices(new ManagedDispatcherImpl(null)))
  428. {
  429. var t = Test();
  430. var cts = new CancellationTokenSource();
  431. Task.Delay(3000).ContinueWith(_ => cts.Cancel());
  432. Dispatcher.UIThread.MainLoop(cts.Token);
  433. Assert.True(t.IsCompletedSuccessfully);
  434. t.GetAwaiter().GetResult();
  435. }
  436. }
  437. }