From 6a99ca39f905280a93e4470508e99cc530acf9d1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 17 Mar 2023 13:04:35 +0600 Subject: [PATCH 01/17] Implemented dispatcher that works like WPF one --- Avalonia.Desktop.slnf | 11 +- .../src/OSX/platformthreading.mm | 316 +++++----- .../AvaloniaSynchronizationContext.cs | 2 +- .../Threading/Dispatcher.Invoke.cs | 541 ++++++++++++++++++ .../Threading/Dispatcher.Queue.cs | 203 +++++++ .../Threading/Dispatcher.Timers.cs | 171 ++++++ src/Avalonia.Base/Threading/Dispatcher.cs | 232 ++++---- .../Threading/DispatcherOperation.cs | 270 +++++++++ .../Threading/DispatcherPriority.cs | 38 +- .../Threading/DispatcherPriorityQueue.cs | 418 ++++++++++++++ .../Threading/DispatcherTimer.cs | 423 +++++++++----- src/Avalonia.Base/Threading/IDispatcher.cs | 25 - .../Threading/IDispatcherClock.cs | 13 + .../Threading/IDispatcherImpl.cs | 90 +++ src/Avalonia.Base/Threading/JobRunner.cs | 2 +- src/Avalonia.Native/AvaloniaNativePlatform.cs | 5 +- src/Avalonia.Native/CallbackBase.cs | 7 +- src/Avalonia.Native/DispatcherImpl.cs | 127 ++++ .../PlatformThreadingInterface.cs | 115 ---- src/Avalonia.Native/avn.idl | 15 +- src/Avalonia.X11/X11Platform.cs | 4 +- src/Avalonia.X11/X11PlatformThreading.cs | 159 ++--- src/Avalonia.X11/X11Window.cs | 3 +- src/Shared/RawEventGrouping.cs | 95 ++- .../Interop/UnmanagedMethods.cs | 43 ++ .../Avalonia.Win32/Win32DispatcherImpl.cs | 121 ++++ src/Windows/Avalonia.Win32/Win32Platform.cs | 70 +-- .../Composition/CompositionAnimationTests.cs | 13 +- .../DispatcherTests.cs | 180 ++++++ .../Rendering/RenderLoopTests.cs | 5 +- .../ToolTipTests.cs | 2 +- .../Xaml/StyleTests.cs | 1 + .../XamlTestBase.cs | 2 + .../Avalonia.UnitTests/UnitTestApplication.cs | 4 +- 34 files changed, 2953 insertions(+), 773 deletions(-) create mode 100644 src/Avalonia.Base/Threading/Dispatcher.Invoke.cs create mode 100644 src/Avalonia.Base/Threading/Dispatcher.Queue.cs create mode 100644 src/Avalonia.Base/Threading/Dispatcher.Timers.cs create mode 100644 src/Avalonia.Base/Threading/DispatcherOperation.cs create mode 100644 src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs create mode 100644 src/Avalonia.Base/Threading/IDispatcherClock.cs create mode 100644 src/Avalonia.Base/Threading/IDispatcherImpl.cs create mode 100644 src/Avalonia.Native/DispatcherImpl.cs delete mode 100644 src/Avalonia.Native/PlatformThreadingInterface.cs create mode 100644 src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs create mode 100644 tests/Avalonia.Base.UnitTests/DispatcherTests.cs diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 477aaec6a8..a87b83de8b 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -8,9 +8,9 @@ "samples\\GpuInterop\\GpuInterop.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", + "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "samples\\SampleControls\\ControlSamples.csproj", "samples\\Sandbox\\Sandbox.csproj", - "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "src\\Avalonia.Base\\Avalonia.Base.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", @@ -38,12 +38,13 @@ "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj", "src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj", - "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", - "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", - "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", + "src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj", + "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", + "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", + "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", @@ -63,4 +64,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} +} \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 6d5bd4aa02..b727b9a6cf 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -1,193 +1,235 @@ #include "common.h" class PlatformThreadingInterface; + + +class LoopCancellation : public ComSingleObject +{ +public: + FORWARD_IUNKNOWN() + + bool Running = false; + bool Cancelled = false; + bool IsApp = false; + + virtual void Cancel() override + { + Cancelled = true; + if(Running) + { + Running = false; + if(![NSThread isMainThread]) + { + AddRef(); + dispatch_async(dispatch_get_main_queue(), ^{ + if(Release() == 0) + return; + Cancel(); + }); + return; + }; + if(IsApp) + [NSApp stop:nil]; + else + { + // Wakeup the event loop + NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:event atStart:YES]; + } + } + }; +}; + +// CFRunLoopTimerSetNextFireDate docs recommend to "create a repeating timer with an initial +// firing time in the distant future (or the initial firing time) and a very large repeat +// interval—on the order of decades or more" +static double distantFutureInterval = (double)50*365*24*3600; + @interface Signaler : NSObject --(void) setParent: (PlatformThreadingInterface*)parent; --(void) signal: (int) priority; +-(void) setEvents:(IAvnPlatformThreadingInterfaceEvents*) events; +-(void) updateTimer:(int)ms; -(Signaler*) init; +-(void) destroyObserver; +-(void) signal; @end -@implementation ActionCallback +@implementation Signaler { - ComPtr _callback; - + ComPtr _events; + bool _wakeupDelegateSent; + bool _signaled; + CFRunLoopObserverRef _observer; + CFRunLoopTimerRef _timer; } -- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback + +- (Signaler*) init { - _callback = callback; + _observer = CFRunLoopObserverCreateWithHandler(nil, + kCFRunLoopBeforeSources | kCFRunLoopAfterWaiting, + true, 0, + ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + bool signaled; + @synchronized (self) { + signaled = self->_signaled; + } + if(signaled) + { + self->_events->Signaled(); + @synchronized (self) { + self->_signaled = false; + } + } + }); + CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); + + + _timer = CFRunLoopTimerCreateWithHandler(nil, CFAbsoluteTimeGetCurrent() + distantFutureInterval, distantFutureInterval, 0, 0, ^(CFRunLoopTimerRef timer) { + self->_events->Timer(); + }); + + CFRunLoopAddTimer(CFRunLoopGetMain(), _timer, kCFRunLoopCommonModes); + return self; } -- (void) action +- (void) destroyObserver { - _callback->Run(); + if(_observer != nil) + { + CFRunLoopObserverInvalidate(_observer); + CFRelease(_observer); + _observer = nil; + } + + if(_timer != nil) + { + CFRunLoopTimerInvalidate(_timer); + CFRelease(_timer); + _timer = nil; + } } +-(void) updateTimer:(int)ms +{ + if(_timer == nil) + return; + double interval = ms < 0 ? distantFutureInterval : ((double)ms / 1000); + CFRunLoopTimerSetTolerance(_timer, 0); + CFRunLoopTimerSetNextFireDate(_timer, CFAbsoluteTimeGetCurrent() + interval); +} -@end +- (void) setEvents: (IAvnPlatformThreadingInterfaceEvents*) events +{ + _events = events; +} -class TimerWrapper : public ComUnknownObject +- (void) signal { - NSTimer* _timer; -public: - TimerWrapper(IAvnActionCallback* callback, int ms) - { - auto cb = [[ActionCallback alloc] initWithCallback:callback]; - _timer = [NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)(double)ms/1000 target:cb selector:@selector(action) userInfo:nullptr repeats:true]; - } - - virtual ~TimerWrapper() - { - [_timer invalidate]; + @synchronized (self) { + if(_signaled) + return; + _signaled = true; + CFRunLoopWakeUp(CFRunLoopGetMain()); } -}; +} +@end class PlatformThreadingInterface : public ComSingleObject { private: + ComPtr _events; Signaler* _signaler; - bool _wasRunningAtLeastOnce = false; - - class LoopCancellation : public ComSingleObject - { - public: - FORWARD_IUNKNOWN() - - bool Running = false; - bool Cancelled = false; - - virtual void Cancel() override - { - Cancelled = true; - if(Running) - { - Running = false; - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSApplication sharedApplication] stop:nil]; - NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined - location:NSMakePoint(0, 0) - modifierFlags:0 - timestamp:0 - windowNumber:0 - context:nil - subtype:0 - data1:0 - data2:0]; - [NSApp postEvent:event atStart:YES]; - }); - } - } - - }; - + CFRunLoopObserverRef _observer = nil; public: FORWARD_IUNKNOWN() - ComPtr SignaledCallback; - PlatformThreadingInterface() { _signaler = [Signaler new]; - [_signaler setParent:this]; - } + }; ~PlatformThreadingInterface() { - if(_signaler) - [_signaler setParent: NULL]; - _signaler = NULL; + [_signaler destroyObserver]; } - virtual bool GetCurrentThreadIsLoopThread() override + bool GetCurrentThreadIsLoopThread() override { return [NSThread isMainThread]; - } - virtual void SetSignaledCallback(IAvnSignaledCallback* cb) override + }; + + bool HasPendingInput() override { - SignaledCallback = cb; - } - virtual IAvnLoopCancellation* CreateLoopCancellation() override + auto event = [NSApp + nextEventMatchingMask: NSEventMaskAny + untilDate:nil + inMode:NSDefaultRunLoopMode + dequeue:false]; + return event != nil; + }; + + void SetEvents(IAvnPlatformThreadingInterfaceEvents *cb) override + { + _events = cb; + [_signaler setEvents:cb]; + }; + + IAvnLoopCancellation *CreateLoopCancellation() override { return new LoopCancellation(); - } + }; - virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override + void RunLoop(IAvnLoopCancellation *cancel) override { START_COM_CALL; - auto can = dynamic_cast(cancel); if(can->Cancelled) - return S_OK; - if(_wasRunningAtLeastOnce) - return E_FAIL; + return; can->Running = true; - _wasRunningAtLeastOnce = true; - [NSApp run]; - return S_OK; - } + if(![NSApp isRunning]) + { + can->IsApp = true; + [NSApp run]; + return; + } + else + { + while(!can->Cancelled) + { + @autoreleasepool + { + NSEvent* ev = [NSApp + nextEventMatchingMask:NSEventMaskAny + untilDate: [NSDate dateWithTimeIntervalSinceNow:1] + inMode:NSDefaultRunLoopMode + dequeue:true]; + if(ev != NULL) + [NSApp sendEvent:ev]; + } + } + } + }; - virtual void Signal(int priority) override + void Signal() override { - [_signaler signal:priority]; - } + [_signaler signal]; + }; - virtual IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback) override + void UpdateTimer(int ms) override { - @autoreleasepool { - - return new TimerWrapper(callback, ms); - } - } -}; - -@implementation Signaler - -PlatformThreadingInterface* _parent = 0; -bool _signaled = 0; -NSArray* _modes; - --(Signaler*) init -{ - if(self = [super init]) - { - _modes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; - } - return self; -} - --(void) perform -{ - ComPtr cb; - @synchronized (self) { - _signaled = false; - if(_parent != NULL) - cb = _parent->SignaledCallback; - } - if(cb != nullptr) - cb->Signaled(0, false); -} - --(void) setParent:(PlatformThreadingInterface *)parent -{ - @synchronized (self) { - _parent = parent; - } -} - --(void) signal: (int) priority -{ - - @synchronized (self) { - if(_signaled) - return; - _signaled = true; - [self performSelector:@selector(perform) onThread:[NSThread mainThread] withObject:NULL waitUntilDone:false modes:_modes]; - } + [_signaler updateTimer:ms]; + }; -} -@end - +}; extern IAvnPlatformThreadingInterface* CreatePlatformThreading() { diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 2cade55f32..b8ac83f418 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -30,7 +30,7 @@ namespace Avalonia.Threading /// public override void Post(SendOrPostCallback d, object? state) { - Dispatcher.UIThread.Post(d, state, DispatcherPriority.Background); + Dispatcher.UIThread.Post(() => d(state), DispatcherPriority.Background); } /// diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs new file mode 100644 index 0000000000..e906931736 --- /dev/null +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -0,0 +1,541 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Threading; + +namespace Avalonia.Threading; + +public partial class Dispatcher +{ + /// + /// Executes the specified Action synchronously on the thread that + /// the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// Note that the default priority is DispatcherPriority.Send. + /// + public void Invoke(Action callback) + { + Invoke(callback, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Action synchronously on the thread that + /// the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + public void Invoke(Action callback, DispatcherPriority priority) + { + Invoke(callback, priority, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Action synchronously on the thread that + /// the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// A cancellation token that can be used to cancel the operation. + /// If the operation has not started, it will be aborted when the + /// cancellation token is canceled. If the operation has started, + /// the operation can cooperate with the cancellation request. + /// + public void Invoke(Action callback, DispatcherPriority priority, CancellationToken cancellationToken) + { + Invoke(callback, priority, cancellationToken, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Action synchronously on the thread that + /// the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// A cancellation token that can be used to cancel the operation. + /// If the operation has not started, it will be aborted when the + /// cancellation token is canceled. If the operation has started, + /// the operation can cooperate with the cancellation request. + /// + /// + /// The minimum amount of time to wait for the operation to start. + /// Once the operation has started, it will complete before this method + /// returns. + /// + public void Invoke(Action callback, DispatcherPriority priority, CancellationToken cancellationToken, + TimeSpan timeout) + { + if (callback == null) + { + throw new ArgumentNullException("callback"); + } + + DispatcherPriority.Validate(priority, "priority"); + + if (timeout.TotalMilliseconds < 0 && + timeout != TimeSpan.FromMilliseconds(-1)) + { + throw new ArgumentOutOfRangeException("timeout"); + } + + // Fast-Path: if on the same thread, and invoking at Send priority, + // and the cancellation token is not already canceled, then just + // call the callback directly. + if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) + { + callback(); + return; + } + + // Slow-Path: go through the queue. + DispatcherOperation operation = new DispatcherOperation(this, priority, callback, false); + InvokeImpl(operation, cancellationToken, timeout); + } + + /// + /// Executes the specified Func synchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The return value from the delegate being invoked. + /// + /// + /// Note that the default priority is DispatcherPriority.Send. + /// + public TResult Invoke(Func callback) + { + return Invoke(callback, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Func synchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// The return value from the delegate being invoked. + /// + public TResult Invoke(Func callback, DispatcherPriority priority) + { + return Invoke(callback, priority, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Func synchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// A cancellation token that can be used to cancel the operation. + /// If the operation has not started, it will be aborted when the + /// cancellation token is canceled. If the operation has started, + /// the operation can cooperate with the cancellation request. + /// + /// + /// The return value from the delegate being invoked. + /// + public TResult Invoke(Func callback, DispatcherPriority priority, + CancellationToken cancellationToken) + { + return Invoke(callback, priority, cancellationToken, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Func synchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// A cancellation token that can be used to cancel the operation. + /// If the operation has not started, it will be aborted when the + /// cancellation token is canceled. If the operation has started, + /// the operation can cooperate with the cancellation request. + /// + /// + /// The minimum amount of time to wait for the operation to start. + /// Once the operation has started, it will complete before this method + /// returns. + /// + /// + /// The return value from the delegate being invoked. + /// + public TResult Invoke(Func callback, DispatcherPriority priority, + CancellationToken cancellationToken, TimeSpan timeout) + { + if (callback == null) + { + throw new ArgumentNullException("callback"); + } + + DispatcherPriority.Validate(priority, "priority"); + + if (timeout.TotalMilliseconds < 0 && + timeout != TimeSpan.FromMilliseconds(-1)) + { + throw new ArgumentOutOfRangeException("timeout"); + } + + // Fast-Path: if on the same thread, and invoking at Send priority, + // and the cancellation token is not already canceled, then just + // call the callback directly. + if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) + { + return callback(); + } + + // Slow-Path: go through the queue. + DispatcherOperation operation = new DispatcherOperation(this, priority, callback); + return (TResult)InvokeImpl(operation, cancellationToken, timeout)!; + } + + /// + /// Executes the specified Action asynchronously on the thread + /// that the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + /// + /// Note that the default priority is DispatcherPriority.Normal. + /// + public DispatcherOperation InvokeAsync(Action callback) + { + return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + } + + /// + /// Executes the specified Action asynchronously on the thread + /// that the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority) + { + return InvokeAsync(callback, priority, CancellationToken.None); + } + + /// + /// Executes the specified Action asynchronously on the thread + /// that the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// A cancellation token that can be used to cancel the operation. + /// If the operation has not started, it will be aborted when the + /// cancellation token is canceled. If the operation has started, + /// the operation can cooperate with the cancellation request. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority, + CancellationToken cancellationToken) + { + if (callback == null) + { + throw new ArgumentNullException("callback"); + } + + DispatcherPriority.Validate(priority, "priority"); + + DispatcherOperation operation = new DispatcherOperation(this, priority, callback, false); + InvokeAsyncImpl(operation, cancellationToken); + + return operation; + } + + /// + /// Executes the specified Func asynchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + /// + /// Note that the default priority is DispatcherPriority.Normal. + /// + public DispatcherOperation InvokeAsync(Func callback) + { + return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + } + + /// + /// Executes the specified Func asynchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + public DispatcherOperation InvokeAsync(Func callback, DispatcherPriority priority) + { + return InvokeAsync(callback, priority, CancellationToken.None); + } + + /// + /// Executes the specified Func asynchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// A cancellation token that can be used to cancel the operation. + /// If the operation has not started, it will be aborted when the + /// cancellation token is canceled. If the operation has started, + /// the operation can cooperate with the cancellation request. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + public DispatcherOperation InvokeAsync(Func callback, DispatcherPriority priority, + CancellationToken cancellationToken) + { + if (callback == null) + { + throw new ArgumentNullException("callback"); + } + + DispatcherPriority.Validate(priority, "priority"); + + DispatcherOperation operation = new DispatcherOperation(this, priority, callback); + InvokeAsyncImpl(operation, cancellationToken); + + return operation; + } + + private void InvokeAsyncImpl(DispatcherOperation operation, CancellationToken cancellationToken) + { + bool succeeded = false; + + // Could be a non-dispatcher thread, lock to read + lock (InstanceLock) + { + if (!cancellationToken.IsCancellationRequested && + !_hasShutdownFinished && + !Environment.HasShutdownStarted) + { + // Add the operation to the work queue + _queue.Enqueue(operation.Priority, operation); + + // Make sure we will wake up to process this operation. + succeeded = RequestProcessing(); + + if (!succeeded) + { + // Dequeue the item since we failed to request + // processing for it. Note we will mark it aborted + // below. + _queue.RemoveItem(operation); + } + } + } + + if (succeeded == true) + { + // We have enqueued the operation. Register a callback + // with the cancellation token to abort the operation + // when cancellation is requested. + if (cancellationToken.CanBeCanceled) + { + CancellationTokenRegistration cancellationRegistration = + cancellationToken.Register(s => ((DispatcherOperation)s!).Abort(), operation); + + // Revoke the cancellation when the operation is done. + operation.Aborted += (s, e) => cancellationRegistration.Dispose(); + operation.Completed += (s, e) => cancellationRegistration.Dispose(); + } + } + else + { + // We failed to enqueue the operation, and the caller that + // created the operation does not expose it before we return, + // so it is safe to modify the operation outside of the lock. + // Just mark the operation as aborted, which we can safely + // return to the user. + operation.DoAbort(); + } + } + + + private object? InvokeImpl(DispatcherOperation operation, CancellationToken cancellationToken, TimeSpan timeout) + { + object? result = null; + + Debug.Assert(timeout.TotalMilliseconds >= 0 || timeout == TimeSpan.FromMilliseconds(-1)); + Debug.Assert(operation.Priority != DispatcherPriority.Send || !CheckAccess()); // should be handled by caller + + if (!cancellationToken.IsCancellationRequested) + { + // This operation must be queued since it was invoked either to + // another thread, or at a priority other than Send. + InvokeAsyncImpl(operation, cancellationToken); + + CancellationToken ctTimeout = CancellationToken.None; + CancellationTokenRegistration ctTimeoutRegistration = new CancellationTokenRegistration(); + CancellationTokenSource? ctsTimeout = null; + + if (timeout.TotalMilliseconds >= 0) + { + // Create a CancellationTokenSource that will abort the + // operation after the timeout. Note that this does not + // cancel the operation, just abort it if it is still pending. + ctsTimeout = new CancellationTokenSource(timeout); + ctTimeout = ctsTimeout.Token; + ctTimeoutRegistration = ctTimeout.Register(s => ((DispatcherOperation)s!).Abort(), operation); + } + + + // We have already registered with the cancellation tokens + // (both provided by the user, and one for the timeout) to + // abort the operation when they are canceled. If the + // operation has already started when the timeout expires, + // we still wait for it to complete. This is different + // than simply waiting on the operation with a timeout + // because we are the ones queueing the dispatcher + // operation, not the caller. We can't leave the operation + // in a state that it might execute if we return that it did not + // invoke. + try + { + operation.GetTask().Wait(); + + Debug.Assert(operation.Status == DispatcherOperationStatus.Completed || + operation.Status == DispatcherOperationStatus.Aborted); + + // Old async semantics return from Wait without + // throwing an exception if the operation was aborted. + // There is no need to test the timout condition, since + // the old async semantics would just return the result, + // which would be null. + + // This should not block because either the operation + // is using the old async sematics, or the operation + // completed successfully. + result = operation.GetResult(); + } + catch (OperationCanceledException) + { + Debug.Assert(operation.Status == DispatcherOperationStatus.Aborted); + + // New async semantics will throw an exception if the + // operation was aborted. Here we convert that + // exception into a timeout exception if the timeout + // has expired (admittedly a weak relationship + // assuming causality). + if (ctTimeout.IsCancellationRequested) + { + // The operation was canceled because of the + // timeout, throw a TimeoutException instead. + throw new TimeoutException(); + } + else + { + // The operation was canceled from some other reason. + throw; + } + } + finally + { + ctTimeoutRegistration.Dispose(); + if (ctsTimeout != null) + { + ctsTimeout.Dispose(); + } + } + } + + return result; + } + + /// + public void Post(Action action, DispatcherPriority priority = default) + { + _ = action ?? throw new ArgumentNullException(nameof(action)); + InvokeAsyncImpl(new DispatcherOperation(this, priority, action, true), CancellationToken.None); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs new file mode 100644 index 0000000000..bf34e80b61 --- /dev/null +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -0,0 +1,203 @@ +using System; +using System.Diagnostics; + +namespace Avalonia.Threading; + +public partial class Dispatcher +{ + private readonly DispatcherPriorityQueue _queue = new(); + private bool _signaled; + private DispatcherTimer? _backgroundTimer; + private const int MaximumTimeProcessingBackgroundJobs = 50; + + void RequestBackgroundProcessing() + { + if (_backgroundTimer == null) + { + _backgroundTimer = + new DispatcherTimer(this, DispatcherPriority.Send, + TimeSpan.FromMilliseconds(1)); + _backgroundTimer.Tick += delegate + { + _backgroundTimer.Stop(); + }; + } + + _backgroundTimer.IsEnabled = true; + } + + /// + /// Force-runs all dispatcher operations ignoring any pending OS events, use with caution + /// + public void RunJobs(DispatcherPriority? priority = null) + { + priority ??= DispatcherPriority.MinimumActiveValue; + if (priority < DispatcherPriority.MinimumActiveValue) + priority = DispatcherPriority.MinimumActiveValue; + while (true) + { + DispatcherOperation? job; + lock (InstanceLock) + job = _queue.Peek(); + if (job == null) + return; + if (priority != null && job.Priority < priority.Value) + return; + ExecuteJob(job); + } + } + + internal static void ResetForUnitTests() + { + if (s_uiThread == null) + return; + var st = Stopwatch.StartNew(); + while (true) + { + if (st.Elapsed.TotalSeconds > 5) + throw new InvalidProgramException("You've caused dispatcher loop"); + + DispatcherOperation? job; + lock (s_uiThread.InstanceLock) + job = s_uiThread._queue.Peek(); + if (job == null || job.Priority <= DispatcherPriority.Inactive) + { + s_uiThread = null; + return; + } + + s_uiThread.ExecuteJob(job); + } + + } + + private void ExecuteJob(DispatcherOperation job) + { + lock (InstanceLock) + _queue.RemoveItem(job); + job.Execute(); + // The backend might be firing timers with a low priority, + // so we manually check if our high priority timers are due for execution + PromoteTimers(); + } + + private void Signaled() + { + try + { + ExecuteJobsCore(); + } + finally + { + lock (InstanceLock) + _signaled = false; + } + } + + void ExecuteJobsCore() + { + int? backgroundJobExecutionStartedAt = null; + while (true) + { + DispatcherOperation? job; + + lock (InstanceLock) + job = _queue.Peek(); + + if (job == null || job.Priority < DispatcherPriority.MinimumActiveValue) + return; + + + // We don't stop for executing jobs queued with >Input priority + if (job.Priority > DispatcherPriority.Input) + { + ExecuteJob(job); + backgroundJobExecutionStartedAt = null; + } + // If platform supports pending input query, ask the platform if we can continue running low priority jobs + else if (_pendingInputImpl?.CanQueryPendingInput == true) + { + if (!_pendingInputImpl.HasPendingInput) + ExecuteJob(job); + else + { + RequestBackgroundProcessing(); + return; + } + } + // We can't check if there is pending input, but still need to enforce interactivity + // so we stop processing background jobs after some timeout and start a timer to continue later + else + { + if (backgroundJobExecutionStartedAt == null) + backgroundJobExecutionStartedAt = Clock.TickCount; + + if (Clock.TickCount - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) + { + _signaled = true; + RequestBackgroundProcessing(); + return; + } + else + ExecuteJob(job); + } + } + } + + private bool RequestProcessing() + { + lock (InstanceLock) + { + if (_queue.MaxPriority <= DispatcherPriority.Input) + RequestBackgroundProcessing(); + else + RequestForegroundProcessing(); + } + return true; + } + + private void RequestForegroundProcessing() + { + if (!_signaled) + { + _signaled = true; + _impl.Signal(); + } + } + + internal void Abort(DispatcherOperation operation) + { + lock (InstanceLock) + _queue.RemoveItem(operation); + operation.DoAbort(); + } + + // Returns whether or not the priority was set. + internal bool SetPriority(DispatcherOperation operation, DispatcherPriority priority) // NOTE: should be Priority + { + bool notify = false; + + lock(InstanceLock) + { + if(operation.IsQueued) + { + _queue.ChangeItemPriority(operation, priority); + notify = true; + + if(notify) + { + // Make sure we will wake up to process this operation. + RequestProcessing(); + + } + } + } + return notify; + } + + public bool HasJobsWithPriority(DispatcherPriority priority) + { + lock (InstanceLock) + return _queue.MaxPriority >= priority; + } +} diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs new file mode 100644 index 0000000000..07c063ee31 --- /dev/null +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Threading; + +public partial class Dispatcher +{ + private List _timers = new(); + private long _timersVersion; + private bool _dueTimeFound; + private int _dueTimeInTicks; + private bool _isOsTimerSet; + + internal void UpdateOSTimer() + { + if (!CheckAccess()) + { + Post(UpdateOSTimer, DispatcherPriority.Send); + return; + } + + lock (InstanceLock) + { + if (!_hasShutdownFinished) // Dispatcher thread, does not technically need the lock to read + { + bool oldDueTimeFound = _dueTimeFound; + int oldDueTimeInTicks = _dueTimeInTicks; + _dueTimeFound = false; + _dueTimeInTicks = 0; + + if (_timers.Count > 0) + { + // We could do better if we sorted the list of timers. + for (int i = 0; i < _timers.Count; i++) + { + var timer = _timers[i]; + + if (!_dueTimeFound || timer.DueTimeInMs - _dueTimeInTicks < 0) + { + _dueTimeFound = true; + _dueTimeInTicks = timer.DueTimeInMs; + } + } + } + + if (_dueTimeFound) + { + if (!_isOsTimerSet || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInTicks)) + { + _impl.UpdateTimer(Math.Max(1, _dueTimeInTicks)); + _isOsTimerSet = true; + } + } + else if (oldDueTimeFound) + { + _impl.UpdateTimer(null); + _isOsTimerSet = false; + } + } + } + } + + internal void AddTimer(DispatcherTimer timer) + { + lock (InstanceLock) + { + if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read + { + _timers.Add(timer); + _timersVersion++; + } + } + + UpdateOSTimer(); + } + + internal void RemoveTimer(DispatcherTimer timer) + { + lock (InstanceLock) + { + if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read + { + _timers.Remove(timer); + _timersVersion++; + } + } + + UpdateOSTimer(); + } + + private void OnOSTimer() + { + lock (InstanceLock) + { + _impl.UpdateTimer(null); + _isOsTimerSet = false; + } + PromoteTimers(); + } + + internal void PromoteTimers() + { + int currentTimeInTicks = Clock.TickCount; + try + { + List? timers = null; + long timersVersion = 0; + + lock (InstanceLock) + { + if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read + { + if (_dueTimeFound && _dueTimeInTicks - currentTimeInTicks <= 0) + { + timers = _timers; + timersVersion = _timersVersion; + } + } + } + + if (timers != null) + { + DispatcherTimer? timer = null; + int iTimer = 0; + + do + { + lock (InstanceLock) + { + timer = null; + + // If the timers collection changed while we are in the middle of + // looking for timers, start over. + if (timersVersion != _timersVersion) + { + timersVersion = _timersVersion; + iTimer = 0; + } + + while (iTimer < _timers.Count) + { + // WARNING: this is vulnerable to wrapping + if (timers[iTimer].DueTimeInMs - currentTimeInTicks <= 0) + { + // Remove this timer from our list. + // Do not increment the index. + timer = timers[iTimer]; + timers.RemoveAt(iTimer); + break; + } + else + { + iTimer++; + } + } + } + + // Now that we are outside of the lock, promote the timer. + if (timer != null) + { + timer.Promote(); + } + } while (timer != null); + } + } + finally + { + UpdateOSTimer(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 571f782813..14808f00a1 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -1,159 +1,119 @@ using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; -using System.Threading.Tasks; using Avalonia.Platform; -namespace Avalonia.Threading +namespace Avalonia.Threading; + +/// +/// Provides services for managing work items on a thread. +/// +/// +/// In Avalonia, there is usually only a single in the application - +/// the one for the UI thread, retrieved via the property. +/// +public partial class Dispatcher : IDispatcher { - /// - /// Provides services for managing work items on a thread. - /// - /// - /// In Avalonia, there is usually only a single in the application - - /// the one for the UI thread, retrieved via the property. - /// - public class Dispatcher : IDispatcher + private readonly IDispatcherImpl _impl; + internal IDispatcherClock Clock { get; } + internal object InstanceLock { get; } = new(); + private bool _hasShutdownFinished; + private readonly IControlledDispatcherImpl? _controlledImpl; + private static Dispatcher? s_uiThread; + private readonly IDispatcherImplWithPendingInput? _pendingInputImpl; + + internal Dispatcher(IDispatcherImpl impl, IDispatcherClock clock) { - private readonly JobRunner _jobRunner; - private IPlatformThreadingInterface? _platform; - - public static Dispatcher UIThread { get; } = - new Dispatcher(AvaloniaLocator.Current.GetService()); - - public Dispatcher(IPlatformThreadingInterface? platform) - { - _platform = platform; - _jobRunner = new JobRunner(platform); - - if (_platform != null) - { - _platform.Signaled += _jobRunner.RunJobs; - } - } - - /// - /// Checks that the current thread is the UI thread. - /// - public bool CheckAccess() => _platform?.CurrentThreadIsLoopThread ?? true; - - /// - /// Checks that the current thread is the UI thread and throws if not. - /// - /// - /// The current thread is not the UI thread. - /// - public void VerifyAccess() - { - if (!CheckAccess()) - throw new InvalidOperationException("Call from invalid thread"); - } - - /// - /// Runs the dispatcher's main loop. - /// - /// - /// A cancellation token used to exit the main loop. - /// - public void MainLoop(CancellationToken cancellationToken) - { - var platform = AvaloniaLocator.Current.GetRequiredService(); - cancellationToken.Register(() => platform.Signal(DispatcherPriority.Send)); - platform.RunLoop(cancellationToken); - } - - /// - /// Runs continuations pushed on the loop. - /// - public void RunJobs() - { - _jobRunner.RunJobs(null); - } - - /// - /// Use this method to ensure that more prioritized tasks are executed - /// - /// - public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority); - - /// - /// Use this method to check if there are more prioritized tasks - /// - /// - public bool HasJobsWithPriority(DispatcherPriority minimumPriority) => - _jobRunner.HasJobsWithPriority(minimumPriority); - - /// - public Task InvokeAsync(Action action, DispatcherPriority priority = default) - { - _ = action ?? throw new ArgumentNullException(nameof(action)); - return _jobRunner.InvokeAsync(action, priority); - } - - /// - public Task InvokeAsync(Func function, DispatcherPriority priority = default) - { - _ = function ?? throw new ArgumentNullException(nameof(function)); - return _jobRunner.InvokeAsync(function, priority); - } - - /// - public Task InvokeAsync(Func function, DispatcherPriority priority = default) - { - _ = function ?? throw new ArgumentNullException(nameof(function)); - return _jobRunner.InvokeAsync(function, priority).Unwrap(); - } + _impl = impl; + Clock = clock; + impl.Timer += OnOSTimer; + impl.Signaled += Signaled; + _controlledImpl = _impl as IControlledDispatcherImpl; + _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; + } + + public static Dispatcher UIThread => s_uiThread ??= CreateUIThreadDispatcher(); - /// - public Task InvokeAsync(Func> function, DispatcherPriority priority = default) + private static Dispatcher CreateUIThreadDispatcher() + { + var impl = AvaloniaLocator.Current.GetService(); + if (impl == null) { - _ = function ?? throw new ArgumentNullException(nameof(function)); - return _jobRunner.InvokeAsync(function, priority).Unwrap(); + var platformThreading = AvaloniaLocator.Current.GetService(); + if (platformThreading != null) + impl = new LegacyDispatcherImpl(platformThreading); + else + impl = new NullDispatcherImpl(); } + return new Dispatcher(impl, impl as IDispatcherClock ?? new DefaultDispatcherClock()); + } - /// - public void Post(Action action, DispatcherPriority priority = default) - { - _ = action ?? throw new ArgumentNullException(nameof(action)); - _jobRunner.Post(action, priority); - } + /// + /// Checks that the current thread is the UI thread. + /// + public bool CheckAccess() => _impl?.CurrentThreadIsLoopThread ?? true; - /// - public void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default) + /// + /// Checks that the current thread is the UI thread and throws if not. + /// + /// + /// The current thread is not the UI thread. + /// + public void VerifyAccess() + { + if (!CheckAccess()) { - _ = action ?? throw new ArgumentNullException(nameof(action)); - _jobRunner.Post(action, arg, priority); - } + // Used to inline VerifyAccess. + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowVerifyAccess() + => throw new InvalidOperationException("Call from invalid thread"); - /// - /// This is needed for platform backends that don't have internal priority system (e. g. win32) - /// To ensure that there are no jobs with higher priority - /// - /// - internal void EnsurePriority(DispatcherPriority currentPriority) - { - if (currentPriority == DispatcherPriority.MaxValue) - return; - currentPriority += 1; - _jobRunner.RunJobs(currentPriority); + ThrowVerifyAccess(); } + } - /// - /// Allows unit tests to change the platform threading interface. - /// - internal void UpdateServices() + internal void Shutdown() + { + DispatcherOperation? operation = null; + _impl.Timer -= PromoteTimers; + _impl.Signaled -= Signaled; + do { - if (_platform != null) + lock(InstanceLock) { - _platform.Signaled -= _jobRunner.RunJobs; + if(_queue.MaxPriority != DispatcherPriority.Invalid) + { + operation = _queue.Peek(); + } + else + { + operation = null; + } } - _platform = AvaloniaLocator.Current.GetService(); - _jobRunner.UpdateServices(); - - if (_platform != null) + if(operation != null) { - _platform.Signaled += _jobRunner.RunJobs; + operation.Abort(); } - } + } while(operation != null); + _impl.UpdateTimer(null); + _hasShutdownFinished = true; + } + + /// + /// Runs the dispatcher's main loop. + /// + /// + /// A cancellation token used to exit the main loop. + /// + public void MainLoop(CancellationToken cancellationToken) + { + if (_controlledImpl == null) + throw new PlatformNotSupportedException(); + cancellationToken.Register(() => RequestProcessing()); + _controlledImpl.RunLoop(cancellationToken); } } diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs new file mode 100644 index 0000000000..5d2eab5a59 --- /dev/null +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -0,0 +1,270 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Threading; + +public class DispatcherOperation +{ + protected readonly bool ThrowOnUiThread; + public DispatcherOperationStatus Status { get; protected set; } + public Dispatcher Dispatcher { get; } + + public DispatcherPriority Priority + { + get => _priority; + set + { + _priority = value; + // Dispatcher is null in ctor + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + Dispatcher?.SetPriority(this, value); + } + } + + protected object? Callback; + protected object? TaskSource; + + internal DispatcherOperation? SequentialPrev { get; set; } + internal DispatcherOperation? SequentialNext { get; set; } + internal DispatcherOperation? PriorityPrev { get; set; } + internal DispatcherOperation? PriorityNext { get; set; } + internal PriorityChain? Chain { get; set; } + + internal bool IsQueued => Chain != null; + + private EventHandler? _aborted; + private EventHandler? _completed; + private DispatcherPriority _priority; + + internal DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Action callback, bool throwOnUiThread) : + this(dispatcher, priority, throwOnUiThread) + { + Callback = callback; + } + + private protected DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, bool throwOnUiThread) + { + ThrowOnUiThread = throwOnUiThread; + Priority = priority; + Dispatcher = dispatcher; + } + + /// + /// An event that is raised when the operation is aborted or canceled. + /// + public event EventHandler Aborted + { + add + { + lock (Dispatcher.InstanceLock) + { + _aborted += value; + } + } + + remove + { + lock(Dispatcher.InstanceLock) + { + _aborted -= value; + } + } + } + + /// + /// An event that is raised when the operation completes. + /// + /// + /// Completed indicates that the operation was invoked and has + /// either completed successfully or faulted. Note that a canceled + /// or aborted operation is never is never considered completed. + /// + public event EventHandler Completed + { + add + { + lock (Dispatcher.InstanceLock) + { + _completed += value; + } + } + + remove + { + lock(Dispatcher.InstanceLock) + { + _completed -= value; + } + } + } + + public void Abort() + { + lock (Dispatcher.InstanceLock) + { + if (Status == DispatcherOperationStatus.Pending) + return; + Dispatcher.Abort(this); + } + } + + public void Wait() + { + if (Dispatcher.CheckAccess()) + throw new InvalidOperationException("Wait is only supported on background thread"); + GetTask().Wait(); + } + + public Task GetTask() => GetTaskCore(); + + /// + /// Returns an awaiter for awaiting the completion of the operation. + /// + /// + /// This method is intended to be used by compilers. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public TaskAwaiter GetAwaiter() + { + return GetTask().GetAwaiter(); + } + + internal void DoAbort() + { + Status = DispatcherOperationStatus.Aborted; + AbortTask(); + _aborted?.Invoke(this, EventArgs.Empty); + } + + internal void Execute() + { + lock (Dispatcher.InstanceLock) + { + Status = DispatcherOperationStatus.Executing; + } + + try + { + InvokeCore(); + } + finally + { + _completed?.Invoke(this, EventArgs.Empty); + } + } + + protected virtual void InvokeCore() + { + try + { + ((Action)Callback!)(); + lock (Dispatcher.InstanceLock) + { + Status = DispatcherOperationStatus.Completed; + if (TaskSource is TaskCompletionSource tcs) + tcs.SetResult(null); + } + } + catch (Exception e) + { + lock (Dispatcher.InstanceLock) + { + Status = DispatcherOperationStatus.Completed; + if (TaskSource is TaskCompletionSource tcs) + tcs.SetException(e); + } + + if (ThrowOnUiThread) + throw; + } + } + + internal virtual object? GetResult() => null; + + protected virtual void AbortTask() => (TaskSource as TaskCompletionSource)?.SetCanceled(); + + private static CancellationToken CreateCancelledToken() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + return cts.Token; + } + + private static readonly Task s_abortedTask = Task.FromCanceled(CreateCancelledToken()); + + protected virtual Task GetTaskCore() + { + lock (Dispatcher.InstanceLock) + { + if (Status == DispatcherOperationStatus.Aborted) + return s_abortedTask; + if (Status == DispatcherOperationStatus.Completed) + return Task.CompletedTask; + if (TaskSource is not TaskCompletionSource tcs) + TaskSource = tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + return tcs.Task; + } + } +} + +public class DispatcherOperation : DispatcherOperation +{ + public DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Func callback) : base(dispatcher, priority, false) + { + TaskSource = new TaskCompletionSource(); + Callback = callback; + } + + private TaskCompletionSource TaskCompletionSource => (TaskCompletionSource)TaskSource!; + + public new Task GetTask() => TaskCompletionSource!.Task; + + protected override Task GetTaskCore() => GetTask(); + + protected override void AbortTask() => TaskCompletionSource.SetCanceled(); + + internal override object? GetResult() => GetTask().Result; + + protected override void InvokeCore() + { + try + { + var result = ((Func)Callback!)(); + lock (Dispatcher.InstanceLock) + { + Status = DispatcherOperationStatus.Completed; + TaskCompletionSource.SetResult(result); + } + } + catch (Exception e) + { + lock (Dispatcher.InstanceLock) + { + Status = DispatcherOperationStatus.Completed; + TaskCompletionSource.SetException(e); + } + } + } + + public T Result + { + get + { + if (TaskCompletionSource.Task.IsCompleted) + return TaskCompletionSource.Task.GetAwaiter().GetResult(); + throw new InvalidOperationException("Synchronous wait is only supported on non-UI threads"); + + } + } +} + +public enum DispatcherOperationStatus +{ + Pending = 0, + Aborted = 1, + Completed = 2, + Executing = 3, +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index b3194e249b..9d0b91f6f2 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -16,31 +16,49 @@ namespace Avalonia.Threading { Value = value; } - + /// - /// Minimum possible priority + /// Minimum possible priority that's actually dispatched, default value /// - public static readonly DispatcherPriority MinValue = new(0); + internal static readonly DispatcherPriority MinimumActiveValue = new(0); + /// + /// A dispatcher priority for jobs that shouldn't be executed yet + /// + public static DispatcherPriority Inactive => new(MinimumActiveValue - 1); + /// + /// Minimum valid priority + /// + internal static readonly DispatcherPriority MinValue = new(Inactive); + + /// + /// Used internally in dispatcher code + /// + public static DispatcherPriority Invalid => new(MinimumActiveValue - 2); + + + /// /// The job will be processed when the system is idle. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue; + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinimumActiveValue; /// /// The job will be processed when the application is idle. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue; + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = new (SystemIdle + 1); /// /// The job will be processed after background operations have completed. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue; + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = new(ApplicationIdle + 1); /// /// The job will be processed with normal priority. /// - public static readonly DispatcherPriority Normal = MinValue; +#pragma warning disable CS0618 + public static readonly DispatcherPriority Normal = new(ContextIdle + 1); +#pragma warning restore CS0618 /// /// The job will be processed after other non-idle operations have completed. @@ -127,5 +145,11 @@ namespace Avalonia.Threading /// public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value); + + public static void Validate(DispatcherPriority priority, string parameterName) + { + if (priority < Inactive || priority > MaxValue) + throw new ArgumentException("Invalid DispatcherPriority value", parameterName); + } } } diff --git a/src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs b/src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs new file mode 100644 index 0000000000..524b4fab8d --- /dev/null +++ b/src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs @@ -0,0 +1,418 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Threading; + +namespace Avalonia.Threading; + + +internal class DispatcherPriorityQueue +{ + // Priority chains... + private readonly SortedList _priorityChains; // NOTE: should be Priority + private readonly Stack _cacheReusableChains; + + // Sequential chain... + private DispatcherOperation? _head; + private DispatcherOperation? _tail; + + public DispatcherPriorityQueue() + { + // Build the collection of priority chains. + _priorityChains = new SortedList(); // NOTE: should be Priority + _cacheReusableChains = new Stack(10); + + _head = _tail = null; + } + + // NOTE: not used + // public int Count {get{return _count;}} + + public DispatcherPriority MaxPriority // NOTE: should be Priority + { + get + { + int count = _priorityChains.Count; + + if (count > 0) + { + return _priorityChains.Keys[count - 1]; + } + else + { + return DispatcherPriority.Invalid; // NOTE: should be Priority.Invalid; + } + } + } + + public DispatcherOperation Enqueue(DispatcherPriority priority, DispatcherOperation item) // NOTE: should be Priority + { + // Find the existing chain for this priority, or create a new one + // if one does not exist. + PriorityChain chain = GetChain(priority); + + // Step 1: Append this to the end of the "sequential" linked list. + InsertItemInSequentialChain(item, _tail); + + // Step 2: Append the item into the priority chain. + InsertItemInPriorityChain(item, chain, chain.Tail); + + return item; + } + + public DispatcherOperation Dequeue() + { + // Get the max-priority chain. + int count = _priorityChains.Count; + if (count > 0) + { + PriorityChain chain = _priorityChains.Values[count - 1]; + Debug.Assert(chain != null, "PriorityQueue.Dequeue: a chain should exist."); + + DispatcherOperation? item = chain.Head; + Debug.Assert(item != null, "PriorityQueue.Dequeue: a priority item should exist."); + + RemoveItem(item); + + return item; + } + else + { + throw new InvalidOperationException(); + } + } + + public DispatcherOperation? Peek() + { + // Get the max-priority chain. + int count = _priorityChains.Count; + if (count > 0) + { + PriorityChain chain = _priorityChains.Values[count - 1]; + Debug.Assert(chain != null, "PriorityQueue.Peek: a chain should exist."); + + DispatcherOperation? item = chain.Head; + Debug.Assert(item != null, "PriorityQueue.Peek: a priority item should exist."); + + return item; + } + + return null; + } + + public void RemoveItem(DispatcherOperation item) + { + Debug.Assert(item != null, "PriorityQueue.RemoveItem: invalid item."); + Debug.Assert(item.Chain != null, "PriorityQueue.RemoveItem: a chain should exist."); + + // Step 1: Remove the item from its priority chain. + RemoveItemFromPriorityChain(item); + + // Step 2: Remove the item from the sequential chain. + RemoveItemFromSequentialChain(item); + } + + public void ChangeItemPriority(DispatcherOperation item, DispatcherPriority priority) // NOTE: should be Priority + { + // Remove the item from its current priority and insert it into + // the new priority chain. Note that this does not change the + // sequential ordering. + + // Step 1: Remove the item from the priority chain. + RemoveItemFromPriorityChain(item); + + // Step 2: Insert the item into the new priority chain. + // Find the existing chain for this priority, or create a new one + // if one does not exist. + PriorityChain chain = GetChain(priority); + InsertItemInPriorityChain(item, chain); + } + + private PriorityChain GetChain(DispatcherPriority priority) // NOTE: should be Priority + { + PriorityChain? chain = null; + + int count = _priorityChains.Count; + if (count > 0) + { + if (priority == _priorityChains.Keys[0]) + { + chain = _priorityChains.Values[0]; + } + else if (priority == _priorityChains.Keys[count - 1]) + { + chain = _priorityChains.Values[count - 1]; + } + else if ((priority > _priorityChains.Keys[0]) && + (priority < _priorityChains.Keys[count - 1])) + { + _priorityChains.TryGetValue(priority, out chain); + } + } + + if (chain == null) + { + if (_cacheReusableChains.Count > 0) + { + chain = _cacheReusableChains.Pop(); + chain.Priority = priority; + } + else + { + chain = new PriorityChain(priority); + } + + _priorityChains.Add(priority, chain); + } + + return chain; + } + + private void InsertItemInPriorityChain(DispatcherOperation item, PriorityChain chain) + { + // Scan along the sequential chain, in the previous direction, + // looking for an item that is already in the new chain. We will + // insert ourselves after the item we found. We can short-circuit + // this search if the new chain is empty. + if (chain.Head == null) + { + Debug.Assert(chain.Tail == null, + "PriorityQueue.InsertItemInPriorityChain: both the head and the tail should be null."); + InsertItemInPriorityChain(item, chain, null); + } + else + { + Debug.Assert(chain.Tail != null, + "PriorityQueue.InsertItemInPriorityChain: both the head and the tail should not be null."); + + DispatcherOperation? after; + + // Search backwards along the sequential chain looking for an + // item already in this list. + for (after = item.SequentialPrev; after != null; after = after.SequentialPrev) + { + if (after.Chain == chain) + { + break; + } + } + + InsertItemInPriorityChain(item, chain, after); + } + } + + internal void InsertItemInPriorityChain(DispatcherOperation item, PriorityChain chain, DispatcherOperation? after) + { + Debug.Assert(chain != null, "PriorityQueue.InsertItemInPriorityChain: a chain must be provided."); + Debug.Assert(item.Chain == null && item.PriorityPrev == null && item.PriorityNext == null, + "PriorityQueue.InsertItemInPriorityChain: item must not already be in a priority chain."); + + item.Chain = chain; + + if (after == null) + { + // Note: passing null for after means insert at the head. + + if (chain.Head != null) + { + Debug.Assert(chain.Tail != null, + "PriorityQueue.InsertItemInPriorityChain: both the head and the tail should not be null."); + + chain.Head.PriorityPrev = item; + item.PriorityNext = chain.Head; + chain.Head = item; + } + else + { + Debug.Assert(chain.Tail == null, + "PriorityQueue.InsertItemInPriorityChain: both the head and the tail should be null."); + + chain.Head = chain.Tail = item; + } + } + else + { + item.PriorityPrev = after; + + if (after.PriorityNext != null) + { + item.PriorityNext = after.PriorityNext; + after.PriorityNext.PriorityPrev = item; + after.PriorityNext = item; + } + else + { + Debug.Assert(item.Chain.Tail == after, + "PriorityQueue.InsertItemInPriorityChain: the chain's tail should be the item we are inserting after."); + after.PriorityNext = item; + chain.Tail = item; + } + } + + chain.Count++; + } + + private void RemoveItemFromPriorityChain(DispatcherOperation item) + { + Debug.Assert(item != null, "PriorityQueue.RemoveItemFromPriorityChain: invalid item."); + Debug.Assert(item.Chain != null, "PriorityQueue.RemoveItemFromPriorityChain: a chain should exist."); + + // Step 1: Fix up the previous link + if (item.PriorityPrev != null) + { + Debug.Assert(item.Chain.Head != item, + "PriorityQueue.RemoveItemFromPriorityChain: the head should not point to this item."); + + item.PriorityPrev.PriorityNext = item.PriorityNext; + } + else + { + Debug.Assert(item.Chain.Head == item, + "PriorityQueue.RemoveItemFromPriorityChain: the head should point to this item."); + + item.Chain.Head = item.PriorityNext; + } + + // Step 2: Fix up the next link + if (item.PriorityNext != null) + { + Debug.Assert(item.Chain.Tail != item, + "PriorityQueue.RemoveItemFromPriorityChain: the tail should not point to this item."); + + item.PriorityNext.PriorityPrev = item.PriorityPrev; + } + else + { + Debug.Assert(item.Chain.Tail == item, + "PriorityQueue.RemoveItemFromPriorityChain: the tail should point to this item."); + + item.Chain.Tail = item.PriorityPrev; + } + + // Step 3: cleanup + item.PriorityPrev = item.PriorityNext = null; + item.Chain.Count--; + if (item.Chain.Count == 0) + { + if (item.Chain.Priority == _priorityChains.Keys[_priorityChains.Count - 1]) + { + _priorityChains.RemoveAt(_priorityChains.Count - 1); + } + else + { + _priorityChains.Remove(item.Chain.Priority); + } + + if (_cacheReusableChains.Count < 10) + { + item.Chain.Priority = DispatcherPriority.Invalid; + _cacheReusableChains.Push(item.Chain); + } + } + + item.Chain = null; + } + + internal void InsertItemInSequentialChain(DispatcherOperation item, DispatcherOperation? after) + { + Debug.Assert(item.SequentialPrev == null && item.SequentialNext == null, + "PriorityQueue.InsertItemInSequentialChain: item must not already be in the sequential chain."); + + if (after == null) + { + // Note: passing null for after means insert at the head. + + if (_head != null) + { + Debug.Assert(_tail != null, + "PriorityQueue.InsertItemInSequentialChain: both the head and the tail should not be null."); + + _head.SequentialPrev = item; + item.SequentialNext = _head; + _head = item; + } + else + { + Debug.Assert(_tail == null, + "PriorityQueue.InsertItemInSequentialChain: both the head and the tail should be null."); + + _head = _tail = item; + } + } + else + { + item.SequentialPrev = after; + + if (after.SequentialNext != null) + { + item.SequentialNext = after.SequentialNext; + after.SequentialNext.SequentialPrev = item; + after.SequentialNext = item; + } + else + { + Debug.Assert(_tail == after, + "PriorityQueue.InsertItemInSequentialChain: the tail should be the item we are inserting after."); + after.SequentialNext = item; + _tail = item; + } + } + } + + private void RemoveItemFromSequentialChain(DispatcherOperation item) + { + Debug.Assert(item != null, "PriorityQueue.RemoveItemFromSequentialChain: invalid item."); + + // Step 1: Fix up the previous link + if (item.SequentialPrev != null) + { + Debug.Assert(_head != item, + "PriorityQueue.RemoveItemFromSequentialChain: the head should not point to this item."); + + item.SequentialPrev.SequentialNext = item.SequentialNext; + } + else + { + Debug.Assert(_head == item, + "PriorityQueue.RemoveItemFromSequentialChain: the head should point to this item."); + + _head = item.SequentialNext; + } + + // Step 2: Fix up the next link + if (item.SequentialNext != null) + { + Debug.Assert(_tail != item, + "PriorityQueue.RemoveItemFromSequentialChain: the tail should not point to this item."); + + item.SequentialNext.SequentialPrev = item.SequentialPrev; + } + else + { + Debug.Assert(_tail == item, + "PriorityQueue.RemoveItemFromSequentialChain: the tail should point to this item."); + + _tail = item.SequentialPrev; + } + + // Step 3: cleanup + item.SequentialPrev = item.SequentialNext = null; + } +} + + +internal class PriorityChain +{ + public PriorityChain(DispatcherPriority priority) // NOTE: should be Priority + { + Priority = priority; + } + + public DispatcherPriority Priority { get; set; } // NOTE: should be Priority + + public int Count { get; set; } + + public DispatcherOperation? Head { get; set; } + + public DispatcherOperation? Tail { get; set; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index f81229eb48..183f66eb61 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -1,207 +1,352 @@ using System; using Avalonia.Reactive; -using Avalonia.Platform; -namespace Avalonia.Threading +namespace Avalonia.Threading; + +/// +/// A timer that is integrated into the Dispatcher queues, and will +/// be processed after a given amount of time at a specified priority. +/// +public partial class DispatcherTimer { /// - /// A timer that uses a to fire at a specified interval. + /// Creates a timer that uses theUI thread's Dispatcher2 to + /// process the timer event at background priority. /// - public class DispatcherTimer + public DispatcherTimer() : this(DispatcherPriority.Background) { - private IDisposable? _timer; + } - private readonly DispatcherPriority _priority; + /// + /// Creates a timer that uses the UI thread's Dispatcher2 to + /// process the timer event at the specified priority. + /// + /// + /// The priority to process the timer at. + /// + public DispatcherTimer(DispatcherPriority priority) : this(Threading.Dispatcher.UIThread, priority, + TimeSpan.FromMilliseconds(0)) + { + } - private TimeSpan _interval; + /// + /// Creates a timer that uses the specified Dispatcher2 to + /// process the timer event at the specified priority. + /// + /// + /// The priority to process the timer at. + /// + /// + /// The dispatcher to use to process the timer. + /// + internal DispatcherTimer(DispatcherPriority priority, Dispatcher dispatcher) : this(dispatcher, priority, + TimeSpan.FromMilliseconds(0)) + { + } - /// - /// Initializes a new instance of the class. - /// - public DispatcherTimer() : this(DispatcherPriority.Background) + /// + /// Creates a timer that uses the UI thread's Dispatcher2 to + /// process the timer event at the specified priority after the specified timeout. + /// + /// + /// The interval to tick the timer after. + /// + /// + /// The priority to process the timer at. + /// + /// + /// The callback to call when the timer ticks. + /// + public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback) + : this(Threading.Dispatcher.UIThread, priority, interval) + { + if (callback == null) { + throw new ArgumentNullException("callback"); } - /// - /// Initializes a new instance of the class. - /// - /// The priority to use. - public DispatcherTimer(DispatcherPriority priority) - { - _priority = priority; - } + Tick += callback; + Start(); + } - /// - /// Initializes a new instance of the class. - /// - /// The interval at which to tick. - /// The priority to use. - /// The event to call when the timer ticks. - public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback) : this(priority) - { - _priority = priority; - Interval = interval; - Tick += callback; - } + /// + /// Gets the dispatcher this timer is associated with. + /// + public Dispatcher Dispatcher + { + get { return _dispatcher; } + } - /// - /// Finalizes an instance of the class. - /// - ~DispatcherTimer() + /// + /// Gets or sets whether the timer is running. + /// + public bool IsEnabled + { + get { return _isEnabled; } + + set { - if (_timer != null) + lock (_instanceLock) { - Stop(); + if (!value && _isEnabled) + { + Stop(); + } + else if (value && !_isEnabled) + { + Start(); + } } } + } - /// - /// Raised when the timer ticks. - /// - public event EventHandler? Tick; + /// + /// Gets or sets the time between timer ticks. + /// + public TimeSpan Interval + { + get { return _interval; } - /// - /// Gets or sets the interval at which the timer ticks. - /// - public TimeSpan Interval + set { - get + bool updateOSTimer = false; + + if (value.TotalMilliseconds < 0) + throw new ArgumentOutOfRangeException("value", + "TimeSpan period must be greater than or equal to zero."); + + if (value.TotalMilliseconds > Int32.MaxValue) + throw new ArgumentOutOfRangeException("value", + "TimeSpan period must be less than or equal to Int32.MaxValue."); + + lock (_instanceLock) { - return _interval; + _interval = value; + + if (_isEnabled) + { + DueTimeInMs = _dispatcher.Clock.TickCount + (int)_interval.TotalMilliseconds; + updateOSTimer = true; + } } - set + if (updateOSTimer) { - bool enabled = IsEnabled; - Stop(); - _interval = value; - IsEnabled = enabled; + _dispatcher.UpdateOSTimer(); } } + } - /// - /// Gets or sets a value indicating whether the timer is running. - /// - public bool IsEnabled + /// + /// Starts the timer. + /// + public void Start() + { + lock (_instanceLock) { - get + if (!_isEnabled) { - return _timer != null; + _isEnabled = true; + + Restart(); } + } + } - set + /// + /// Stops the timer. + /// + public void Stop() + { + bool updateOSTimer = false; + + lock (_instanceLock) + { + if (_isEnabled) { - if (IsEnabled != value) + _isEnabled = false; + updateOSTimer = true; + + // If the operation is in the queue, abort it. + if (_operation != null) { - if (value) - { - Start(); - } - else - { - Stop(); - } + _operation.Abort(); + _operation = null; } } } - /// - /// Gets or sets user-defined data associated with the timer. - /// - public object? Tag + if (updateOSTimer) { - get; - set; + _dispatcher.RemoveTimer(this); } + } + + /// + /// Starts a new timer. + /// + /// + /// The method to call on timer tick. If the method returns false, the timer will stop. + /// + /// The interval at which to tick. + /// The priority to use. + /// An used to cancel the timer. + public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = default) + { + var timer = new DispatcherTimer(priority) { Interval = interval }; - /// - /// Starts a new timer. - /// - /// - /// The method to call on timer tick. If the method returns false, the timer will stop. - /// - /// The interval at which to tick. - /// The priority to use. - /// An used to cancel the timer. - public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = default) + timer.Tick += (s, e) => { - var timer = new DispatcherTimer(priority) { Interval = interval }; - - timer.Tick += (s, e) => + if (!action()) { - if (!action()) - { - timer.Stop(); - } - }; + timer.Stop(); + } + }; - timer.Start(); + timer.Start(); - return Disposable.Create(() => timer.Stop()); - } + return Disposable.Create(() => timer.Stop()); + } + + /// + /// Runs a method once, after the specified interval. + /// + /// + /// The method to call after the interval has elapsed. + /// + /// The interval after which to call the method. + /// The priority to use. + /// An used to cancel the timer. + public static IDisposable RunOnce( + Action action, + TimeSpan interval, + DispatcherPriority priority = default) + { + interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); + + var timer = new DispatcherTimer(priority) { Interval = interval }; - /// - /// Runs a method once, after the specified interval. - /// - /// - /// The method to call after the interval has elapsed. - /// - /// The interval after which to call the method. - /// The priority to use. - /// An used to cancel the timer. - public static IDisposable RunOnce( - Action action, - TimeSpan interval, - DispatcherPriority priority = default) + timer.Tick += (s, e) => { - interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); - - var timer = new DispatcherTimer(priority) { Interval = interval }; + action(); + timer.Stop(); + }; + + timer.Start(); + + return Disposable.Create(() => timer.Stop()); + } + + /// + /// Occurs when the specified timer interval has elapsed and the + /// timer is enabled. + /// + public event EventHandler? Tick; + + /// + /// Any data that the caller wants to pass along with the timer. + /// + public object? Tag { get; set; } - timer.Tick += (s, e) => - { - action(); - timer.Stop(); - }; - timer.Start(); + internal DispatcherTimer(Dispatcher dispatcher, DispatcherPriority priority, TimeSpan interval) + { + if (dispatcher == null) + { + throw new ArgumentNullException("dispatcher"); + } - return Disposable.Create(() => timer.Stop()); + DispatcherPriority.Validate(priority, "priority"); + if (priority == DispatcherPriority.Inactive) + { + throw new ArgumentException("Specified priority is not valid.", "priority"); } - /// - /// Starts the timer. - /// - public void Start() + if (interval.TotalMilliseconds < 0) + throw new ArgumentOutOfRangeException("interval", "TimeSpan period must be greater than or equal to zero."); + + if (interval.TotalMilliseconds > Int32.MaxValue) + throw new ArgumentOutOfRangeException("interval", + "TimeSpan period must be less than or equal to Int32.MaxValue."); + + + _dispatcher = dispatcher; + _priority = priority; + _interval = interval; + } + + private void Restart() + { + lock (_instanceLock) { - if (!IsEnabled) + if (_operation != null) { - var threading = AvaloniaLocator.Current.GetRequiredService(); - _timer = threading.StartTimer(_priority, Interval, InternalTick); + // Timer has already been restarted, e.g. Start was called form the Tick handler. + return; + } + + // BeginInvoke a new operation. + _operation = _dispatcher.InvokeAsync(FireTick, DispatcherPriority.Inactive); + + DueTimeInMs = _dispatcher.Clock.TickCount + (int)_interval.TotalMilliseconds; + + if (_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess()) + { + // shortcut - just promote the item now + Promote(); + } + else + { + _dispatcher.AddTimer(this); } } + } - /// - /// Stops the timer. - /// - public void Stop() + internal void Promote() // called from Dispatcher + { + lock (_instanceLock) { - if (IsEnabled) + // Simply promote the operation to it's desired priority. + if (_operation != null) { - _timer!.Dispose(); - _timer = null; + _operation.Priority = _priority; } } + } + private void FireTick() + { + // The operation has been invoked, so forget about it. + _operation = null; + // The dispatcher thread is calling us because item's priority + // was changed from inactive to something else. + if (Tick != null) + { + Tick(this, EventArgs.Empty); + } - /// - /// Raises the event on the dispatcher thread. - /// - private void InternalTick() + // If we are still enabled, start the timer again. + if (_isEnabled) { - Dispatcher.UIThread.EnsurePriority(_priority); - Tick?.Invoke(this, EventArgs.Empty); + Restart(); } } -} + + // This is the object we use to synchronize access. + private object _instanceLock = new object(); + + // Note: We cannot BE a dispatcher-affinity object because we can be + // created by a worker thread. We are still associated with a + // dispatcher (where we post the item) but we can be accessed + // by any thread. + private Dispatcher _dispatcher; + + private DispatcherPriority _priority; // NOTE: should be Priority + private TimeSpan _interval; + private DispatcherOperation? _operation; + private bool _isEnabled; + + // used by Dispatcher + internal int DueTimeInMs { get; private set; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs index 1c700132b7..e2a3115f9c 100644 --- a/src/Avalonia.Base/Threading/IDispatcher.cs +++ b/src/Avalonia.Base/Threading/IDispatcher.cs @@ -26,30 +26,5 @@ namespace Avalonia.Threading /// The method. /// The priority with which to invoke the method. void Post(Action action, DispatcherPriority priority = default); - - /// - /// Posts an action that will be invoked on the dispatcher thread. - /// - /// The method. - /// The argument of method to call. - /// The priority with which to invoke the method. - void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default); - - /// - /// Invokes a action on the dispatcher thread. - /// - /// The method. - /// The priority with which to invoke the method. - /// A task that can be used to track the method's execution. - Task InvokeAsync(Action action, DispatcherPriority priority = default); - - /// - /// Queues the specified work to run on the dispatcher thread and returns a proxy for the - /// task returned by . - /// - /// The work to execute asynchronously. - /// The priority with which to invoke the method. - /// A task that represents a proxy for the task returned by . - Task InvokeAsync(Func function, DispatcherPriority priority = default); } } diff --git a/src/Avalonia.Base/Threading/IDispatcherClock.cs b/src/Avalonia.Base/Threading/IDispatcherClock.cs new file mode 100644 index 0000000000..2a5268d192 --- /dev/null +++ b/src/Avalonia.Base/Threading/IDispatcherClock.cs @@ -0,0 +1,13 @@ +using System; + +namespace Avalonia.Threading; + +internal interface IDispatcherClock +{ + int TickCount { get; } +} + +internal class DefaultDispatcherClock : IDispatcherClock +{ + public int TickCount => Environment.TickCount; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs new file mode 100644 index 0000000000..5d83ced011 --- /dev/null +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading; +using Avalonia.Platform; + +namespace Avalonia.Threading; + +interface IDispatcherImpl +{ + + //IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick); + + bool CurrentThreadIsLoopThread { get; } + + // Asynchronously triggers Signaled callback + void Signal(); + event Action Signaled; + event Action Timer; + void UpdateTimer(int? dueTimeInTicks); +} + + +interface IDispatcherImplWithPendingInput : IDispatcherImpl +{ + // Checks if dispatcher implementation can + bool CanQueryPendingInput { get; } + // Checks if there is pending user input + bool HasPendingInput { get; } +} + +interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput +{ + // Runs the event loop + void RunLoop(CancellationToken token); +} + +internal class LegacyDispatcherImpl : IControlledDispatcherImpl +{ + private readonly IPlatformThreadingInterface _platformThreading; + private IDisposable? _timer; + + public LegacyDispatcherImpl(IPlatformThreadingInterface platformThreading) + { + _platformThreading = platformThreading; + _platformThreading.Signaled += delegate { Signaled?.Invoke(); }; + } + + public bool CurrentThreadIsLoopThread => _platformThreading.CurrentThreadIsLoopThread; + public void Signal() => _platformThreading.Signal(DispatcherPriority.Send); + + public event Action? Signaled; + public event Action? Timer; + public void UpdateTimer(int? dueTimeInTicks) + { + _timer?.Dispose(); + _timer = null; + if (dueTimeInTicks.HasValue) + _timer = _platformThreading.StartTimer(DispatcherPriority.Send, + TimeSpan.FromMilliseconds(dueTimeInTicks.Value), + OnTick); + } + + private void OnTick() + { + _timer?.Dispose(); + _timer = null; + Timer?.Invoke(); + } + + public bool CanQueryPendingInput => false; + public bool HasPendingInput => false; + public void RunLoop(CancellationToken token) => _platformThreading.RunLoop(token); +} + +class NullDispatcherImpl : IDispatcherImpl +{ + public bool CurrentThreadIsLoopThread => true; + + public void Signal() + { + + } + + public event Action? Signaled; + public event Action? Timer; + + public void UpdateTimer(int? dueTimeInTicks) + { + + } +} diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs index ced3423c27..b2b4f876d1 100644 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ b/src/Avalonia.Base/Threading/JobRunner.cs @@ -28,7 +28,7 @@ namespace Avalonia.Threading /// Priority to execute jobs for. Pass null if platform doesn't have internal priority system public void RunJobs(DispatcherPriority? priority) { - var minimumPriority = priority ?? DispatcherPriority.MinValue; + var minimumPriority = priority ?? DispatcherPriority.MinimumActiveValue; while (true) { var job = GetNextJob(minimumPriority); diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 67b55bd512..e6a103f6b9 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -9,6 +9,7 @@ using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +using Avalonia.Threading; using MicroCom.Runtime; #nullable enable @@ -98,8 +99,8 @@ namespace Avalonia.Native } AvaloniaLocator.CurrentMutable - .Bind() - .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) + .Bind() + .ToConstant(new DispatcherImpl(_factory.CreatePlatformThreadingInterface())) .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) diff --git a/src/Avalonia.Native/CallbackBase.cs b/src/Avalonia.Native/CallbackBase.cs index 9f59c97f9b..c5978e2a0d 100644 --- a/src/Avalonia.Native/CallbackBase.cs +++ b/src/Avalonia.Native/CallbackBase.cs @@ -2,6 +2,7 @@ using System.Runtime.ExceptionServices; using Avalonia.MicroCom; using Avalonia.Platform; +using Avalonia.Threading; using MicroCom.Runtime; namespace Avalonia.Native @@ -10,11 +11,9 @@ namespace Avalonia.Native { public void RaiseException(Exception e) { - if (AvaloniaLocator.Current.GetService() is PlatformThreadingInterface threadingInterface) + if (AvaloniaLocator.Current.GetService() is DispatcherImpl dispatcherImpl) { - threadingInterface.TerminateNativeApp(); - - threadingInterface.DispatchException(ExceptionDispatchInfo.Capture(e)); + dispatcherImpl.PropagateCallbackException(ExceptionDispatchInfo.Capture(e)); } } } diff --git a/src/Avalonia.Native/DispatcherImpl.cs b/src/Avalonia.Native/DispatcherImpl.cs new file mode 100644 index 0000000000..4788dd5e82 --- /dev/null +++ b/src/Avalonia.Native/DispatcherImpl.cs @@ -0,0 +1,127 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.ExceptionServices; +using System.Threading; +using Avalonia.Native.Interop; +using Avalonia.Threading; +using MicroCom.Runtime; + +namespace Avalonia.Native; + +internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock +{ + private readonly IAvnPlatformThreadingInterface _native; + private Thread? _loopThread; + private Stack _managedFrames = new(); + + public DispatcherImpl(IAvnPlatformThreadingInterface native) + { + _native = native; + using var events = new Events(this); + _native.SetEvents(events); + } + + public event Action Signaled; + public event Action Timer; + + private class Events : NativeCallbackBase, IAvnPlatformThreadingInterfaceEvents + { + private readonly DispatcherImpl _parent; + + public Events(DispatcherImpl parent) + { + _parent = parent; + } + public void Signaled() => _parent.Signaled?.Invoke(); + + public void Timer() => _parent.Timer?.Invoke(); + } + + public bool CurrentThreadIsLoopThread + { + get + { + if (_loopThread != null) + return Thread.CurrentThread == _loopThread; + if (_native.CurrentThreadIsLoopThread == 0) + return false; + _loopThread = Thread.CurrentThread; + return true; + } + } + + public void Signal() => _native.Signal(); + + public void UpdateTimer(int? dueTimeInTicks) + { + var ms = dueTimeInTicks == null ? -1 : Math.Max(1, dueTimeInTicks.Value - TickCount); + _native.UpdateTimer(ms); + } + + public bool CanQueryPendingInput => true; + public bool HasPendingInput => _native.HasPendingInput() != 0; + + class RunLoopFrame : IDisposable + { + public ExceptionDispatchInfo? Exception; + public CancellationTokenSource CancellationTokenSource = new(); + + public RunLoopFrame(CancellationToken token) + { + CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + } + + public void Dispose() => CancellationTokenSource.Dispose(); + } + + public void RunLoop(CancellationToken token) + { + if (token.IsCancellationRequested) + return; + object l = new(); + var exited = false; + + using var frame = new RunLoopFrame(token); + + using var cancel = _native.CreateLoopCancellation(); + frame.CancellationTokenSource.Token.Register(() => + { + lock (l) + // ReSharper disable once AccessToModifiedClosure + // ReSharper disable once AccessToDisposedClosure + if (!exited) + cancel.Cancel(); + }); + + try + { + _managedFrames.Push(frame); + _native.RunLoop(cancel); + } + finally + { + lock (l) + exited = true; + _managedFrames.Pop(); + if (frame.Exception != null) + frame.Exception.Throw(); + } + } + + public int TickCount => Environment.TickCount; + + public void PropagateCallbackException(ExceptionDispatchInfo capture) + { + if (_managedFrames.Count == 0) + { + Debug.Assert(false, "We should never get here"); + return; + } + + var frame = _managedFrames.Peek(); + frame.Exception = capture; + frame.CancellationTokenSource.Cancel(); + } +} diff --git a/src/Avalonia.Native/PlatformThreadingInterface.cs b/src/Avalonia.Native/PlatformThreadingInterface.cs deleted file mode 100644 index 99aa8a5850..0000000000 --- a/src/Avalonia.Native/PlatformThreadingInterface.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Runtime.ExceptionServices; -using System.Threading; -using Avalonia.Native.Interop; -using Avalonia.Platform; -using Avalonia.Threading; - -namespace Avalonia.Native -{ - internal class PlatformThreadingInterface : IPlatformThreadingInterface - { - class TimerCallback : NativeCallbackBase, IAvnActionCallback - { - readonly Action _tick; - - public TimerCallback(Action tick) - { - _tick = tick; - } - - public void Run() - { - _tick(); - } - } - - class SignaledCallback : NativeCallbackBase, IAvnSignaledCallback - { - readonly PlatformThreadingInterface _parent; - - public SignaledCallback(PlatformThreadingInterface parent) - { - _parent = parent; - } - - public void Signaled(int priority, int priorityContainsMeaningfulValue) - { - _parent.Signaled?.Invoke(priorityContainsMeaningfulValue.FromComBool() ? (DispatcherPriority?)priority : null); - } - } - - readonly IAvnPlatformThreadingInterface _native; - private ExceptionDispatchInfo _exceptionDispatchInfo; - private CancellationTokenSource _exceptionCancellationSource; - - public PlatformThreadingInterface(IAvnPlatformThreadingInterface native) - { - _native = native; - using (var cb = new SignaledCallback(this)) - _native.SetSignaledCallback(cb); - } - - public bool CurrentThreadIsLoopThread => _native.CurrentThreadIsLoopThread.FromComBool(); - - public event Action Signaled; - - public void RunLoop(CancellationToken cancellationToken) - { - _exceptionDispatchInfo?.Throw(); - var l = new object(); - _exceptionCancellationSource = new CancellationTokenSource(); - - var compositeCancellation = CancellationTokenSource - .CreateLinkedTokenSource(cancellationToken, _exceptionCancellationSource.Token).Token; - - var cancellation = _native.CreateLoopCancellation(); - compositeCancellation.Register(() => - { - lock (l) - { - cancellation?.Cancel(); - } - }); - - try - { - _native.RunLoop(cancellation); - } - finally - { - lock (l) - { - cancellation?.Dispose(); - cancellation = null; - } - } - - if (_exceptionDispatchInfo != null) - { - _exceptionDispatchInfo.Throw(); - } - } - - public void DispatchException (ExceptionDispatchInfo exceptionInfo) - { - _exceptionDispatchInfo = exceptionInfo; - } - - public void TerminateNativeApp() - { - _exceptionCancellationSource?.Cancel(); - } - - public void Signal(DispatcherPriority priority) - { - _native.Signal((int)priority); - } - - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) - { - using (var cb = new TimerCallback(tick)) - return _native.StartTimer((int)priority, (int)interval.TotalMilliseconds, cb); - } - } -} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index b107e83e7a..7763d0d2fc 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -645,9 +645,10 @@ interface IAvnActionCallback : IUnknown } [uuid(6df4d2db-0b80-4f59-ad88-0baa5e21eb14)] -interface IAvnSignaledCallback : IUnknown +interface IAvnPlatformThreadingInterfaceEvents : IUnknown { - void Signaled(int priority, bool priorityContainsMeaningfulValue); + void Signaled(); + void Timer(); } [uuid(97330f88-c22b-4a8e-a130-201520091b01)] @@ -660,12 +661,12 @@ interface IAvnLoopCancellation : IUnknown interface IAvnPlatformThreadingInterface : IUnknown { bool GetCurrentThreadIsLoopThread(); - void SetSignaledCallback(IAvnSignaledCallback* cb); + bool HasPendingInput(); + void SetEvents(IAvnPlatformThreadingInterfaceEvents* cb); IAvnLoopCancellation* CreateLoopCancellation(); - HRESULT RunLoop(IAvnLoopCancellation* cancel); - // Can't pass int* to sharpgentools for some reason - void Signal(int priority); - IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback); + void RunLoop(IAvnLoopCancellation* cancel); + void Signal(); + void UpdateTimer(int ms); } [uuid(6c621a6e-e4c1-4ae3-9749-83eeeffa09b6)] diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 170c6ce6b3..e4e5dbfcb8 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -14,6 +14,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +using Avalonia.Threading; using Avalonia.X11; using Avalonia.X11.Glx; using static Avalonia.X11.XLib; @@ -34,6 +35,7 @@ namespace Avalonia.X11 public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } public X11Globals Globals { get; private set; } + public ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue { get; } = new(); [DllImport("libc")] private static extern void setlocale(int type, string s); public void Initialize(X11PlatformOptions options) @@ -72,7 +74,7 @@ namespace Avalonia.X11 AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(this) - .Bind().ToConstant(new X11PlatformThreading(this)) + .Bind().ToConstant(new X11PlatformThreading(this)) .Bind().ToConstant(new SleepLoopRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) diff --git a/src/Avalonia.X11/X11PlatformThreading.cs b/src/Avalonia.X11/X11PlatformThreading.cs index 1946642de3..b8a5b68658 100644 --- a/src/Avalonia.X11/X11PlatformThreading.cs +++ b/src/Avalonia.X11/X11PlatformThreading.cs @@ -9,7 +9,7 @@ using static Avalonia.X11.XLib; namespace Avalonia.X11 { - internal unsafe class X11PlatformThreading : IPlatformThreadingInterface + internal unsafe class X11PlatformThreading : IControlledDispatcherImpl, IDispatcherClock { private readonly AvaloniaX11Platform _platform; private readonly IntPtr _display; @@ -68,44 +68,11 @@ namespace Avalonia.X11 private int _sigread, _sigwrite; private object _lock = new object(); private bool _signaled; - private DispatcherPriority _signaledPriority; + private bool _wakeupRequested; + private long? _nextTimer; private int _epoll; private Stopwatch _clock = Stopwatch.StartNew(); - private class X11Timer : IDisposable - { - private readonly X11PlatformThreading _parent; - - public X11Timer(X11PlatformThreading parent, DispatcherPriority prio, TimeSpan interval, Action tick) - { - _parent = parent; - Priority = prio; - Tick = tick; - Interval = interval; - Reschedule(); - } - - public DispatcherPriority Priority { get; } - public TimeSpan NextTick { get; private set; } - public TimeSpan Interval { get; } - public Action Tick { get; } - public bool Disposed { get; private set; } - - public void Reschedule() - { - NextTick = _parent._clock.Elapsed + Interval; - } - - public void Dispose() - { - Disposed = true; - lock (_parent._lock) - _parent._timers.Remove(this); - } - } - - private List _timers = new List(); - public X11PlatformThreading(AvaloniaX11Platform platform) { _platform = platform; @@ -139,29 +106,16 @@ namespace Avalonia.X11 throw new X11Exception("Unable to attach signal pipe to epoll"); } - private int TimerComparer(X11Timer t1, X11Timer t2) - { - return t2.Priority - t1.Priority; - } - private void CheckSignaled() { - int buf = 0; - while (read(_sigread, &buf, new IntPtr(4)).ToInt64() > 0) - { - } - - DispatcherPriority prio; lock (_lock) { if (!_signaled) return; _signaled = false; - prio = _signaledPriority; - _signaledPriority = DispatcherPriority.MinValue; } - Signaled?.Invoke(prio); + Signaled?.Invoke(); } private unsafe void HandleX11(CancellationToken cancellationToken) @@ -170,6 +124,7 @@ namespace Avalonia.X11 { if (cancellationToken.IsCancellationRequested) return; + XNextEvent(_display, out var xev); if(XFilterEvent(ref xev, IntPtr.Zero)) continue; @@ -195,90 +150,94 @@ namespace Avalonia.X11 XFreeEventData(_display, &xev.GenericEventCookie); } } - - Dispatcher.UIThread.RunJobs(); } - + public void RunLoop(CancellationToken cancellationToken) { - var readyTimers = new List(); while (!cancellationToken.IsCancellationRequested) { - var now = _clock.Elapsed; - TimeSpan? nextTick = null; - readyTimers.Clear(); - lock(_timers) - foreach (var t in _timers) - { - if (nextTick == null || t.NextTick < nextTick.Value) - nextTick = t.NextTick; - if (t.NextTick < now) - readyTimers.Add(t); - } - - readyTimers.Sort(TimerComparer); - - foreach (var t in readyTimers) + var now = _clock.ElapsedMilliseconds; + if (_nextTimer.HasValue && now > _nextTimer.Value) { - if (cancellationToken.IsCancellationRequested) - return; - t.Tick(); - if(!t.Disposed) - { - t.Reschedule(); - if (nextTick == null || t.NextTick < nextTick.Value) - nextTick = t.NextTick; - } + Timer?.Invoke(); } if (cancellationToken.IsCancellationRequested) return; + //Flush whatever requests were made to XServer XFlush(_display); epoll_event ev; if (XPending(_display) == 0) - epoll_wait(_epoll, &ev, 1, - nextTick == null ? -1 : Math.Max(1, (int)(nextTick.Value - _clock.Elapsed).TotalMilliseconds)); + { + now = _clock.ElapsedMilliseconds; + if (_nextTimer < now) + continue; + + var timeout = _nextTimer == null ? (int)-1 : Math.Max(1, _nextTimer.Value - now); + epoll_wait(_epoll, &ev, 1, (int)Math.Min(int.MaxValue, timeout)); + + // Drain the signaled pipe + int buf = 0; + while (read(_sigread, &buf, new IntPtr(4)).ToInt64() > 0) + { + } + + lock (_lock) + _wakeupRequested = false; + } + if (cancellationToken.IsCancellationRequested) return; CheckSignaled(); HandleX11(cancellationToken); + while (_platform.EventGrouperDispatchQueue.HasJobs) + { + CheckSignaled(); + _platform.EventGrouperDispatchQueue.DispatchNext(); + } } } - + private void Wakeup() + { + lock (_lock) + { + if(_wakeupRequested) + return; + _wakeupRequested = true; + int buf = 0; + write(_sigwrite, &buf, new IntPtr(1)); + } + } - public void Signal(DispatcherPriority priority) + public void Signal() { lock (_lock) { - if (priority > _signaledPriority) - _signaledPriority = priority; - if(_signaled) return; _signaled = true; - int buf = 0; - write(_sigwrite, &buf, new IntPtr(1)); + Wakeup(); } } public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _mainThread; - public event Action Signaled; - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) + public event Action Signaled; + public event Action Timer; + + public void UpdateTimer(int? dueTimeInTicks) { - if (_mainThread != Thread.CurrentThread) - throw new InvalidOperationException("StartTimer can be only called from UI thread"); - if (interval <= TimeSpan.Zero) - throw new ArgumentException("Interval must be positive", nameof(interval)); - - // We assume that we are on the main thread and outside of epoll_wait, so there is no need for wakeup signal - - var timer = new X11Timer(this, priority, interval, tick); - lock(_timers) - _timers.Add(timer); - return timer; + _nextTimer = dueTimeInTicks; + if (_nextTimer != null) + Wakeup(); } + + + public int TickCount => (int)_clock.ElapsedMilliseconds; + public bool CanQueryPendingInput => true; + + public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || XPending(_display) != 0; } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 6634ab4d7b..5fde749099 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -187,7 +187,7 @@ namespace Avalonia.X11 UpdateMotifHints(); UpdateSizeHints(null); - _rawEventGrouper = new RawEventGrouper(DispatchInput); + _rawEventGrouper = new RawEventGrouper(DispatchInput, platform.EventGrouperDispatchQueue); _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals); _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None); @@ -507,7 +507,6 @@ namespace Avalonia.X11 if (changedSize && !updatedSizeViaScaling && !_popup) Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); - Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); }, DispatcherPriority.Layout); if (_useRenderWindow) XConfigureResizeWindow(_x11.Display, _renderHandle, ev.ConfigureEvent.width, diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs index c4772db820..66b24e9722 100644 --- a/src/Shared/RawEventGrouping.cs +++ b/src/Shared/RawEventGrouping.cs @@ -12,28 +12,47 @@ namespace Avalonia; While doing that it groups Move and TouchUpdate events so we could provide GetIntermediatePoints API */ -internal class RawEventGrouper : IDisposable +internal interface IRawEventGrouperDispatchQueue { - private readonly Action _eventCallback; - private readonly Queue _inputQueue = new(); + void Add(RawInputEventArgs args, Action handler); +} + + +class ManualRawEventGrouperDispatchQueue : IRawEventGrouperDispatchQueue +{ + private readonly Queue<(RawInputEventArgs args, Action handler)> _inputQueue = new(); + public void Add(RawInputEventArgs args, Action handler) => _inputQueue.Enqueue((args, handler)); + + public bool HasJobs => _inputQueue.Count > 0; + + public void DispatchNext() + { + if (_inputQueue.Count == 0) + return; + var ev = _inputQueue.Dequeue(); + ev.handler(ev.args); + } +} + +internal class AutomaticRawEventGrouperDispatchQueue : IRawEventGrouperDispatchQueue +{ + private readonly Queue<(RawInputEventArgs args, Action handler)> _inputQueue = new(); private readonly Action _dispatchFromQueue; - private readonly Dictionary _lastTouchPoints = new(); - private RawInputEventArgs? _lastEvent; - public RawEventGrouper(Action eventCallback) + public AutomaticRawEventGrouperDispatchQueue() { - _eventCallback = eventCallback; _dispatchFromQueue = DispatchFromQueue; } - private void AddToQueue(RawInputEventArgs args) + public void Add(RawInputEventArgs args, Action handler) { - _lastEvent = args; - _inputQueue.Enqueue(args); + _inputQueue.Enqueue((args, handler)); + if (_inputQueue.Count == 1) Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input); + } - + private void DispatchFromQueue() { while (true) @@ -43,17 +62,8 @@ internal class RawEventGrouper : IDisposable var ev = _inputQueue.Dequeue(); - if (_lastEvent == ev) - _lastEvent = null; + ev.handler(ev.args); - if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate) - _lastTouchPoints.Remove(touchUpdate.RawPointerId); - - _eventCallback?.Invoke(ev); - - if (ev is RawPointerEventArgs { IntermediatePoints.Value: PooledList list }) - list.Dispose(); - if (Dispatcher.UIThread.HasJobsWithPriority(DispatcherPriority.Input + 1)) { Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input); @@ -61,6 +71,47 @@ internal class RawEventGrouper : IDisposable } } } +} + +internal class RawEventGrouper : IDisposable +{ + private readonly Action _eventCallback; + private readonly IRawEventGrouperDispatchQueue _queue; + private readonly Dictionary _lastTouchPoints = new(); + private RawInputEventArgs? _lastEvent; + private Action _dispatch; + private bool _disposed; + + public RawEventGrouper(Action eventCallback, IRawEventGrouperDispatchQueue? queue = null) + { + _eventCallback = eventCallback; + _queue = queue ?? new AutomaticRawEventGrouperDispatchQueue(); + _dispatch = Dispatch; + } + + private void AddToQueue(RawInputEventArgs args) + { + _lastEvent = args; + _queue.Add(args, _dispatch); + } + + private void Dispatch(RawInputEventArgs ev) + { + if (!_disposed) + { + if (_lastEvent == ev) + _lastEvent = null; + + if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate) + _lastTouchPoints.Remove(touchUpdate.RawPointerId); + + _eventCallback?.Invoke(ev); + } + + if (ev is RawPointerEventArgs { IntermediatePoints.Value: PooledList list }) + list.Dispose(); + } + public void HandleEvent(RawInputEventArgs args) { @@ -123,7 +174,7 @@ internal class RawEventGrouper : IDisposable public void Dispose() { - _inputQueue.Clear(); + _disposed = true; _lastEvent = null; _lastTouchPoints.Clear(); } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 1dd2ef20c7..9618a5b4cd 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1784,6 +1784,49 @@ namespace Avalonia.Win32.Interop return result; } + [Flags] + internal enum QueueStatusFlags + { + QS_KEY = 0x0001, + QS_MOUSEMOVE = 0x0002, + QS_MOUSEBUTTON = 0x0004, + QS_POSTMESSAGE = 0x0008, + QS_TIMER = 0x0010, + QS_PAINT = 0x0020, + QS_SENDMESSAGE = 0x0040, + QS_HOTKEY = 0x0080, + QS_ALLPOSTMESSAGE = 0x0100, + QS_EVENT = 0x0200, + QS_MOUSE = QS_MOUSEMOVE | QS_MOUSEBUTTON, + QS_INPUT = QS_MOUSE | QS_KEY, + QS_ALLEVENTS = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY, + QS_ALLINPUT = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY | QS_SENDMESSAGE + } + + [Flags] + internal enum MsgWaitForMultipleObjectsFlags + { + MWMO_WAITALL = 0x0001, + MWMO_ALERTABLE = 0x0002, + MWMO_INPUTAVAILABLE = 0x0004 + } + + [DllImport("user32", EntryPoint="MsgWaitForMultipleObjectsEx", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto)] + private static extern int IntMsgWaitForMultipleObjectsEx(int nCount, IntPtr[]? pHandles, int dwMilliseconds, + QueueStatusFlags dwWakeMask, MsgWaitForMultipleObjectsFlags dwFlags); + + internal static int MsgWaitForMultipleObjectsEx(int nCount, IntPtr[]? pHandles, int dwMilliseconds, + QueueStatusFlags dwWakeMask, MsgWaitForMultipleObjectsFlags dwFlags) + { + int result = IntMsgWaitForMultipleObjectsEx(nCount, pHandles, dwMilliseconds, dwWakeMask, dwFlags); + if(result == -1) + { + throw new Win32Exception(); + } + + return result; + } + [DllImport("user32.dll")] internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); diff --git a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs new file mode 100644 index 0000000000..0916490ed8 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs @@ -0,0 +1,121 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Avalonia.Threading; +using Avalonia.Win32.Interop; +using static Avalonia.Win32.Interop.UnmanagedMethods; +namespace Avalonia.Win32; + +internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock +{ + private readonly IntPtr _messageWindow; + private static Thread? s_uiThread; + private IntPtr? _timerHandle; + private readonly TimerProc _timerDelegate; + public Win32DispatcherImpl(IntPtr messageWindow) + { + _messageWindow = messageWindow; + s_uiThread = Thread.CurrentThread; + _timerDelegate = TimerProc; + } + + public bool CurrentThreadIsLoopThread => s_uiThread == Thread.CurrentThread; + internal const int SignalW = unchecked((int)0xdeadbeaf); + internal const int SignalL = unchecked((int)0x12345678); + + public void Signal() => + // Messages from PostMessage are always processed before any user input, + // so Win32 should call us ASAP + PostMessage( + _messageWindow, + (int)WindowsMessage.WM_DISPATCH_WORK_ITEM, + new IntPtr(SignalW), + new IntPtr(SignalL)); + + public void DispatchWorkItem() => Signaled?.Invoke(); + + public event Action? Signaled; + public event Action? Timer; + + void TimerProc(IntPtr hWnd, uint uMsg, IntPtr nIdEvent, uint dwTime) => Timer?.Invoke(); + + public void UpdateTimer(int? dueTimeInTicks) + { + if (_timerHandle.HasValue) + KillTimer(IntPtr.Zero, _timerHandle.Value); + if (dueTimeInTicks == null) + return; + + var interval = (uint)Math.Max(1, TickCount - dueTimeInTicks.Value); + + _timerHandle = SetTimer( + IntPtr.Zero, + IntPtr.Zero, + interval, + _timerDelegate); + } + + public bool CanQueryPendingInput => true; + + public bool HasPendingInput + { + get + { + // We need to know if there is any pending input in the Win32 + // queue because we want to only process Avalon "background" + // items after Win32 input has been processed. + // + // Win32 provides the GetQueueStatus API -- but it has a major + // drawback: it only counts "new" input. This means that + // sometimes it could return false, even if there really is input + // that needs to be processed. This results in very hard to + // find bugs. + // + // Luckily, Win32 also provides the MsgWaitForMultipleObjectsEx + // API. While more awkward to use, this API can return queue + // status information even if the input is "old". The various + // flags we use are: + // + // QS_INPUT + // This represents any pending input - such as mouse moves, or + // key presses. It also includes the new GenericInput messages. + // + // QS_EVENT + // This is actually a private flag that represents the various + // events that can be queued in Win32. Some of these events + // can cause input, but Win32 doesn't include them in the + // QS_INPUT flag. An example is WM_MOUSELEAVE. + // + // QS_POSTMESSAGE + // If there is already a message in the queue, we need to process + // it before we can process input. + // + // MWMO_INPUTAVAILABLE + // This flag indicates that any input (new or old) is to be + // reported. + // + + return MsgWaitForMultipleObjectsEx(0, null, 0, + QueueStatusFlags.QS_INPUT | QueueStatusFlags.QS_EVENT | QueueStatusFlags.QS_POSTMESSAGE, + MsgWaitForMultipleObjectsFlags.MWMO_INPUTAVAILABLE) == 0; + } + } + + public void RunLoop(CancellationToken cancellationToken) + { + var result = 0; + while (!cancellationToken.IsCancellationRequested + && (result = GetMessage(out var msg, IntPtr.Zero, 0, 0)) > 0) + { + TranslateMessage(ref msg); + DispatchMessage(ref msg); + } + if (result < 0) + { + Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform) + ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(RunLoop), Marshal.GetLastWin32Error()); + } + } + + public int TickCount => Environment.TickCount; +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 7a9f2bb814..6a5841f36f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -107,21 +107,21 @@ namespace Avalonia namespace Avalonia.Win32 { - internal class Win32Platform : IPlatformThreadingInterface, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl + internal class Win32Platform : IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl { private static readonly Win32Platform s_instance = new(); - private static Thread? s_uiThread; private static Win32PlatformOptions? s_options; private static Compositor? s_compositor; private WndProc? _wndProcDelegate; private IntPtr _hwnd; - private readonly List _delegates = new(); + private Win32DispatcherImpl _dispatcher; public Win32Platform() { SetDpiAwareness(); CreateMessageWindow(); + _dispatcher = new Win32DispatcherImpl(_hwnd); } internal static Win32Platform Instance => s_instance; @@ -157,7 +157,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(WindowsKeyboardDevice.Instance) .Bind().ToSingleton() - .Bind().ToConstant(s_instance) + .Bind().ToConstant(s_instance._dispatcher) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(renderTimer) .Bind().ToConstant(s_instance) @@ -174,8 +174,6 @@ namespace Avalonia.Win32 .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) .Bind().ToConstant(s_instance); - s_uiThread = Thread.CurrentThread; - var platformGraphics = options.CustomPlatformGraphics ?? Win32GlManager.Initialize(); @@ -205,67 +203,15 @@ namespace Avalonia.Win32 } } - public void RunLoop(CancellationToken cancellationToken) - { - var result = 0; - while (!cancellationToken.IsCancellationRequested - && (result = GetMessage(out var msg, IntPtr.Zero, 0, 0)) > 0) - { - TranslateMessage(ref msg); - DispatchMessage(ref msg); - } - if (result < 0) - { - Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform) - ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(RunLoop), Marshal.GetLastWin32Error()); - } - } - - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action callback) - { - TimerProc timerDelegate = (_, _, _, _) => callback(); - - IntPtr handle = SetTimer( - IntPtr.Zero, - IntPtr.Zero, - (uint)interval.TotalMilliseconds, - timerDelegate); - - // Prevent timerDelegate being garbage collected. - _delegates.Add(timerDelegate); - - return Disposable.Create(() => - { - _delegates.Remove(timerDelegate); - KillTimer(IntPtr.Zero, handle); - }); - } - - private const int SignalW = unchecked((int)0xdeadbeaf); - private const int SignalL = unchecked((int)0x12345678); - - public void Signal(DispatcherPriority prio) - { - PostMessage( - _hwnd, - (int)WindowsMessage.WM_DISPATCH_WORK_ITEM, - new IntPtr(SignalW), - new IntPtr(SignalL)); - } - - public bool CurrentThreadIsLoopThread => s_uiThread == Thread.CurrentThread; - - public event Action? Signaled; - public event EventHandler? ShutdownRequested; [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - if (msg == (int)WindowsMessage.WM_DISPATCH_WORK_ITEM && wParam.ToInt64() == SignalW && lParam.ToInt64() == SignalL) - { - Signaled?.Invoke(null); - } + if (msg == (int)WindowsMessage.WM_DISPATCH_WORK_ITEM + && wParam.ToInt64() == Win32DispatcherImpl.SignalW + && lParam.ToInt64() == Win32DispatcherImpl.SignalL) + _dispatcher?.DispatchWorkItem(); if(msg == (uint)WindowsMessage.WM_QUERYENDSESSION) { diff --git a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs index 41c0eb3958..510d7c2c6d 100644 --- a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs @@ -62,13 +62,24 @@ public class CompositionAnimationTests } } } + + class DummyDispatcher : IDispatcher + { + public bool CheckAccess() => true; + + public void VerifyAccess() + { + } + + public void Post(Action action, DispatcherPriority priority = default) => throw new NotSupportedException(); + } [AnimationDataProvider] [Theory] public void GenericCheck(AnimationData data) { var compositor = - new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer(), new Dispatcher(null)), null); + new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer(), new DummyDispatcher()), null); var target = compositor.CreateSolidColorVisual(); var ani = new ScalarKeyFrameAnimation(null); foreach (var frame in data.Frames) diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs new file mode 100644 index 0000000000..450c857cfe --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Threading; +using Xunit; +namespace Avalonia.Base.UnitTests; + +public class DispatcherTests +{ + class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherClock, IDispatcherImplWithPendingInput + { + public bool CurrentThreadIsLoopThread => true; + + public void Signal() => AskedForSignal = true; + + public event Action Signaled; + public event Action Timer; + public int? NextTimer { get; private set; } + public bool AskedForSignal { get; private set; } + + public void UpdateTimer(int? dueTimeInTicks) + { + NextTimer = dueTimeInTicks; + } + + public int TickCount { get; set; } + + public void ExecuteSignal() + { + if (!AskedForSignal) + return; + AskedForSignal = false; + Signaled?.Invoke(); + } + + public void ExecuteTimer() + { + if (NextTimer == null) + return; + TickCount = NextTimer.Value; + Timer?.Invoke(); + } + + public bool CanQueryPendingInput => TestInputPending != null; + public bool HasPendingInput => TestInputPending == true; + public bool? TestInputPending { get; set; } + } + + + [Fact] + public void DispatcherExecutesJobsAccordingToPriority() + { + var impl = new SimpleDispatcherImpl(); + var disp = new Dispatcher(impl, impl); + var actions = new List(); + disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background); + disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render); + disp.Post(()=>actions.Add("Input"), DispatcherPriority.Input); + Assert.True(impl.AskedForSignal); + impl.ExecuteSignal(); + Assert.Equal(new[] { "Render", "Input", "Background" }, actions); + } + + [Fact] + public void DispatcherPreservesOrderWhenChangingPriority() + { + var impl = new SimpleDispatcherImpl(); + var disp = new Dispatcher(impl, impl); + var actions = new List(); + var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background); + var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input); + disp.Post(() => actions.Add("Render"), DispatcherPriority.Render); + + toPromote.Priority = DispatcherPriority.Render; + toPromote2.Priority = DispatcherPriority.Render; + + Assert.True(impl.AskedForSignal); + impl.ExecuteSignal(); + + Assert.Equal(new[] { "PromotedRender", "PromotedRender2", "Render" }, actions); + } + + [Fact] + public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached() + { + var impl = new SimpleDispatcherImpl(); + var disp = new Dispatcher(impl, impl); + var actions = new List(); + for (var c = 0; c < 10; c++) + { + var itemId = c; + disp.Post(() => + { + actions.Add(itemId); + impl.TickCount += 20; + }, DispatcherPriority.Background); + } + + Assert.False(impl.AskedForSignal); + Assert.NotNull(impl.NextTimer); + + impl.ExecuteTimer(); + Assert.True(impl.AskedForSignal); + Assert.Null(impl.NextTimer); + + for (var c = 0; c < 4; c++) + { + if (impl.NextTimer != null) + impl.ExecuteTimer(); + Assert.True(impl.AskedForSignal); + impl.ExecuteSignal(); + var expectedCount = (c + 1) * 3; + if (c == 3) + expectedCount = 10; + + Assert.Equal(Enumerable.Range(0, expectedCount), actions); + Assert.False(impl.AskedForSignal); + if (c < 3) + { + Assert.True(impl.NextTimer > impl.TickCount); + } + else + Assert.Null(impl.NextTimer); + } + } + + + [Fact] + public void DispatcherStopsItemProcessingWhenInputIsPending() + { + var impl = new SimpleDispatcherImpl(); + impl.TestInputPending = false; + var disp = new Dispatcher(impl, impl); + var actions = new List(); + for (var c = 0; c < 10; c++) + { + var itemId = c; + disp.Post(() => + { + actions.Add(itemId); + if (itemId == 0 || itemId == 3 || itemId == 7) + impl.TestInputPending = true; + }, DispatcherPriority.Background); + } + Assert.False(impl.AskedForSignal); + Assert.NotNull(impl.NextTimer); + + impl.ExecuteTimer(); + Assert.True(impl.AskedForSignal); + Assert.Null(impl.NextTimer); + + for (var c = 0; c < 4; c++) + { + if (impl.NextTimer != null) + impl.ExecuteTimer(); + Assert.True(impl.AskedForSignal); + impl.ExecuteSignal(); + var expectedCount = c switch + { + 0 => 1, + 1 => 4, + 2 => 8, + 3 => 10 + }; + + Assert.Equal(Enumerable.Range(0, expectedCount), actions); + Assert.False(impl.AskedForSignal); + if (c < 3) + { + Assert.True(impl.NextTimer > impl.TickCount); + impl.TickCount = impl.NextTimer.Value + 1; + } + else + Assert.Null(impl.NextTimer); + + impl.TestInputPending = false; + } + } + +} \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/Rendering/RenderLoopTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/RenderLoopTests.cs index 6855a378b4..97740fab00 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/RenderLoopTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/RenderLoopTests.cs @@ -47,9 +47,8 @@ namespace Avalonia.Base.UnitTests.Rendering { var dispatcher = new Mock(); dispatcher.Setup( - d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) - .Callback((Action a, DispatcherPriority _) => a()) - .Returns(Task.CompletedTask); + d => d.Post(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => a()); var timer = new Mock(); var loop = new RenderLoop(timer.Object, dispatcher.Object); diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index c196a8f984..c268fed28c 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -157,7 +157,7 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] + [Fact(Skip = "Timers should NOT, in fact, be checked via IPlatformThreadingInterface")] public void Should_Open_On_Pointer_Enter_With_Delay() { Action timercallback = null; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 933958b3a0..3aeb27dd50 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Runtime.CompilerServices; using System.Xml; using Avalonia.Controls; using Avalonia.Markup.Xaml.Styling; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs b/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs index ea03b003ca..6d26de2b29 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; +using Avalonia.Controls; using Avalonia.Data; namespace Avalonia.Markup.Xaml.UnitTests @@ -13,6 +14,7 @@ namespace Avalonia.Markup.Xaml.UnitTests { // Ensure necessary assemblies are loaded. var _ = typeof(TemplateBinding); + GC.KeepAlive(typeof(ItemsRepeater)); if (AvaloniaLocator.Current.GetService() == null) AvaloniaLocator.CurrentMutable.Bind() .ToConstant(new TestXamlLoaderShim()); diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index fe7f9712b4..3108e09315 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -43,7 +43,7 @@ namespace Avalonia.UnitTests { var scope = AvaloniaLocator.EnterScope(); var app = new UnitTestApplication(services); - Dispatcher.UIThread.UpdateServices(); + Dispatcher.ResetForUnitTests(); return Disposable.Create(() => { if (Dispatcher.UIThread.CheckAccess()) @@ -52,7 +52,7 @@ namespace Avalonia.UnitTests } scope.Dispose(); - Dispatcher.UIThread.UpdateServices(); + Dispatcher.ResetForUnitTests(); }); } From 968e61f649270f9f8a7f8db517cd755bdd756132 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 17 Mar 2023 17:48:31 +0600 Subject: [PATCH 02/17] Fixed tests, reintroduced Post with SendOrPostCallback, removed JobRunner --- .../Threading/Dispatcher.Invoke.cs | 12 + .../Threading/Dispatcher.Queue.cs | 16 + .../Threading/Dispatcher.Timers.cs | 4 + src/Avalonia.Base/Threading/Dispatcher.cs | 6 +- .../Threading/DispatcherOperation.cs | 39 +++ src/Avalonia.Base/Threading/JobRunner.cs | 300 ------------------ .../Utilities/DispatcherTimerHelper.cs | 17 + .../Input/GesturesTests.cs | 127 ++++---- .../ToolTipTests.cs | 25 +- 9 files changed, 151 insertions(+), 395 deletions(-) delete mode 100644 src/Avalonia.Base/Threading/JobRunner.cs create mode 100644 src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index e906931736..80b62b3818 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -538,4 +538,16 @@ public partial class Dispatcher _ = action ?? throw new ArgumentNullException(nameof(action)); InvokeAsyncImpl(new DispatcherOperation(this, priority, action, true), CancellationToken.None); } + + /// + /// Posts an action that will be invoked on the dispatcher thread. + /// + /// The method. + /// The argument of method to call. + /// The priority with which to invoke the method. + public void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default) + { + _ = action ?? throw new ArgumentNullException(nameof(action)); + InvokeAsyncImpl(new SendOrPostCallbackDispatcherOperation(this, priority, action, arg, true), CancellationToken.None); + } } \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index bf34e80b61..0ce2479a45 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -47,6 +47,20 @@ public partial class Dispatcher } } + class DummyShuttingDownUnitTestDispatcherImpl : IDispatcherImpl + { + public bool CurrentThreadIsLoopThread => true; + public void Signal() + { + } + + public event Action? Signaled; + public event Action? Timer; + public void UpdateTimer(int? dueTimeInTicks) + { + } + } + internal static void ResetForUnitTests() { if (s_uiThread == null) @@ -54,6 +68,8 @@ public partial class Dispatcher var st = Stopwatch.StartNew(); while (true) { + s_uiThread._pendingInputImpl = s_uiThread._controlledImpl = null; + s_uiThread._impl = new DummyShuttingDownUnitTestDispatcherImpl(); if (st.Elapsed.TotalSeconds > 5) throw new InvalidProgramException("You've caused dispatcher loop"); diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index 07c063ee31..e71e7375d3 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Avalonia.Threading; @@ -168,4 +169,7 @@ public partial class Dispatcher UpdateOSTimer(); } } + + internal static List SnapshotTimersForUnitTests() => + s_uiThread!._timers.Where(t => t != s_uiThread._backgroundTimer).ToList(); } \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 14808f00a1..9a0dacfbb8 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -16,13 +16,13 @@ namespace Avalonia.Threading; /// public partial class Dispatcher : IDispatcher { - private readonly IDispatcherImpl _impl; + private IDispatcherImpl _impl; internal IDispatcherClock Clock { get; } internal object InstanceLock { get; } = new(); private bool _hasShutdownFinished; - private readonly IControlledDispatcherImpl? _controlledImpl; + private IControlledDispatcherImpl? _controlledImpl; private static Dispatcher? s_uiThread; - private readonly IDispatcherImplWithPendingInput? _pendingInputImpl; + private IDispatcherImplWithPendingInput? _pendingInputImpl; internal Dispatcher(IDispatcherImpl impl, IDispatcherClock clock) { diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index 5d2eab5a59..b0d7e5a049 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -261,6 +261,45 @@ public class DispatcherOperation : DispatcherOperation } } +internal class SendOrPostCallbackDispatcherOperation : DispatcherOperation +{ + private readonly object? _arg; + + internal SendOrPostCallbackDispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, + SendOrPostCallback callback, object? arg, bool throwOnUiThread) + : base(dispatcher, priority, throwOnUiThread) + { + Callback = callback; + _arg = arg; + } + + protected override void InvokeCore() + { + try + { + ((SendOrPostCallback)Callback!)(_arg); + lock (Dispatcher.InstanceLock) + { + Status = DispatcherOperationStatus.Completed; + if (TaskSource is TaskCompletionSource tcs) + tcs.SetResult(null); + } + } + catch (Exception e) + { + lock (Dispatcher.InstanceLock) + { + Status = DispatcherOperationStatus.Completed; + if (TaskSource is TaskCompletionSource tcs) + tcs.SetException(e); + } + + if (ThrowOnUiThread) + throw; + } + } +} + public enum DispatcherOperationStatus { Pending = 0, diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs deleted file mode 100644 index b2b4f876d1..0000000000 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ /dev/null @@ -1,300 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Platform; - -namespace Avalonia.Threading -{ - /// - /// A main loop in a . - /// - internal class JobRunner - { - private IPlatformThreadingInterface? _platform; - - private readonly Queue[] _queues = Enumerable.Range(0, (int)DispatcherPriority.MaxValue + 1) - .Select(_ => new Queue()).ToArray(); - - public JobRunner(IPlatformThreadingInterface? platform) - { - _platform = platform; - } - - /// - /// Runs continuations pushed on the loop. - /// - /// Priority to execute jobs for. Pass null if platform doesn't have internal priority system - public void RunJobs(DispatcherPriority? priority) - { - var minimumPriority = priority ?? DispatcherPriority.MinimumActiveValue; - while (true) - { - var job = GetNextJob(minimumPriority); - if (job == null) - return; - - job.Run(); - } - } - - /// - /// Invokes a method on the main loop. - /// - /// The method. - /// The priority with which to invoke the method. - /// A task that can be used to track the method's execution. - public Task InvokeAsync(Action action, DispatcherPriority priority) - { - var job = new Job(action, priority, false); - AddJob(job); - return job.Task!; - } - - /// - /// Invokes a method on the main loop. - /// - /// The method. - /// The priority with which to invoke the method. - /// A task that can be used to track the method's execution. - public Task InvokeAsync(Func function, DispatcherPriority priority) - { - var job = new JobWithResult(function, priority); - AddJob(job); - return job.Task; - } - - /// - /// Post action that will be invoked on main thread - /// - /// The method. - /// - /// The priority with which to invoke the method. - internal void Post(Action action, DispatcherPriority priority) - { - AddJob(new Job(action, priority, true)); - } - - /// - /// Post action that will be invoked on main thread - /// - /// The method to call. - /// The parameter of method to call. - /// The priority with which to invoke the method. - internal void Post(SendOrPostCallback action, object? parameter, DispatcherPriority priority) - { - AddJob(new JobWithArg(action, parameter, priority, true)); - } - - /// - /// Allows unit tests to change the platform threading interface. - /// - internal void UpdateServices() - { - _platform = AvaloniaLocator.Current.GetService(); - } - - private void AddJob(IJob job) - { - bool needWake; - var queue = _queues[(int)job.Priority]; - lock (queue) - { - needWake = queue.Count == 0; - queue.Enqueue(job); - } - if (needWake) - _platform?.Signal(job.Priority); - } - - private IJob? GetNextJob(DispatcherPriority minimumPriority) - { - for (int c = (int)DispatcherPriority.MaxValue; c >= (int)minimumPriority; c--) - { - var q = _queues[c]; - lock (q) - { - if (q.Count > 0) - return q.Dequeue(); - } - } - return null; - } - - public bool HasJobsWithPriority(DispatcherPriority minimumPriority) - { - for (int c = (int)minimumPriority; c < (int)DispatcherPriority.MaxValue; c++) - { - var q = _queues[c]; - lock (q) - { - if (q.Count > 0) - return true; - } - } - - return false; - } - - private interface IJob - { - /// - /// Gets the job priority. - /// - DispatcherPriority Priority { get; } - - /// - /// Runs the job. - /// - void Run(); - } - - /// - /// A job to run. - /// - private sealed class Job : IJob - { - /// - /// The method to call. - /// - private readonly Action _action; - /// - /// The task completion source. - /// - private readonly TaskCompletionSource? _taskCompletionSource; - - /// - /// Initializes a new instance of the class. - /// - /// The method to call. - /// The job priority. - /// Do not wrap exception in TaskCompletionSource - public Job(Action action, DispatcherPriority priority, bool throwOnUiThread) - { - _action = action; - Priority = priority; - _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); - } - - /// - public DispatcherPriority Priority { get; } - - /// - /// The task. - /// - public Task? Task => _taskCompletionSource?.Task; - - /// - void IJob.Run() - { - if (_taskCompletionSource == null) - { - _action(); - return; - } - try - { - _action(); - _taskCompletionSource.SetResult(null); - } - catch (Exception e) - { - _taskCompletionSource.SetException(e); - } - } - } - - /// - /// A typed job to run. - /// - private sealed class JobWithArg : IJob - { - private readonly SendOrPostCallback _action; - private readonly object? _parameter; - private readonly TaskCompletionSource? _taskCompletionSource; - - /// - /// Initializes a new instance of the class. - /// - /// The method to call. - /// The parameter of method to call. - /// The job priority. - /// Do not wrap exception in TaskCompletionSource - - public JobWithArg(SendOrPostCallback action, object? parameter, DispatcherPriority priority, bool throwOnUiThread) - { - _action = action; - _parameter = parameter; - Priority = priority; - _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); - } - - /// - public DispatcherPriority Priority { get; } - - /// - void IJob.Run() - { - if (_taskCompletionSource == null) - { - _action(_parameter); - return; - } - try - { - _action(_parameter); - _taskCompletionSource.SetResult(default); - } - catch (Exception e) - { - _taskCompletionSource.SetException(e); - } - } - } - - /// - /// A job to run thath return value. - /// - /// Type of job result - private sealed class JobWithResult : IJob - { - private readonly Func _function; - private readonly TaskCompletionSource _taskCompletionSource; - - /// - /// Initializes a new instance of the class. - /// - /// The method to call. - /// The job priority. - public JobWithResult(Func function, DispatcherPriority priority) - { - _function = function; - Priority = priority; - _taskCompletionSource = new TaskCompletionSource(); - } - - /// - public DispatcherPriority Priority { get; } - - /// - /// The task. - /// - public Task Task => _taskCompletionSource.Task; - - /// - void IJob.Run() - { - try - { - var result = _function(); - _taskCompletionSource.SetResult(result); - } - catch (Exception e) - { - _taskCompletionSource.SetException(e); - } - } - } - } -} diff --git a/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs b/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs new file mode 100644 index 0000000000..0c4a5f1051 --- /dev/null +++ b/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs @@ -0,0 +1,17 @@ +using Avalonia.Threading; + +namespace Avalonia.Utilities; + +public class DispatcherTimerHelper +{ + +} + +public static class DispatcherTimerUtils +{ + public static void ForceFire(this DispatcherTimer timer) + { + timer.Promote(); + timer.Dispatcher.RunJobs(); + } +} \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs index d466f6a4a5..c53891fa1c 100644 --- a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.GestureRecognizers; @@ -7,14 +8,16 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; +using Avalonia.Utilities; using Moq; using Xunit; +// ReSharper disable RedundantArgumentDefaultValue namespace Avalonia.Base.UnitTests.Input { public class GesturesTests { - private MouseTestHelper _mouse = new MouseTestHelper(); + private readonly MouseTestHelper _mouse = new MouseTestHelper(); [Fact] public void Tapped_Should_Follow_Pointer_Pressed_Released() @@ -60,7 +63,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.TappedEvent, (s, e) => raised = true); + decorator.AddHandler(Gestures.TappedEvent, (_, _) => raised = true); _mouse.Click(border, MouseButton.Middle); @@ -77,7 +80,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.TappedEvent, (s, e) => raised = true); + decorator.AddHandler(Gestures.TappedEvent, (_, _) => raised = true); _mouse.Click(border, MouseButton.Right); @@ -94,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.RightTappedEvent, (s, e) => raised = true); + decorator.AddHandler(Gestures.RightTappedEvent, (_, _) => raised = true); _mouse.Click(border, MouseButton.Right); @@ -147,7 +150,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => raised = true); + decorator.AddHandler(Gestures.DoubleTappedEvent, (_, _) => raised = true); _mouse.Click(border, MouseButton.Middle); _mouse.Down(border, MouseButton.Middle, clickCount: 2); @@ -165,7 +168,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => raised = true); + decorator.AddHandler(Gestures.DoubleTappedEvent, (_, _) => raised = true); _mouse.Click(border, MouseButton.Right); _mouse.Down(border, MouseButton.Right, clickCount: 2); @@ -182,10 +185,8 @@ namespace Avalonia.Base.UnitTests.Input iSettingsMock.Setup(x => x.GetTapSize(It.IsAny())).Returns(new Size(16, 16)); AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(iSettingsMock.Object); - - var scheduledTimers = new List<(TimeSpan time, Action action)>(); - using var app = UnitTestApplication.Start(new TestServices( - threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + using var app = UnitTestApplication.Start(); Border border = new Border(); Gestures.SetIsHoldWithMouseEnabled(border, true); @@ -195,15 +196,15 @@ namespace Avalonia.Base.UnitTests.Input }; HoldingState holding = HoldingState.Cancelled; - decorator.AddHandler(Gestures.HoldingEvent, (s, e) => holding = e.HoldingState); + decorator.AddHandler(Gestures.HoldingEvent, (_, e) => holding = e.HoldingState); _mouse.Down(border); Assert.False(holding != HoldingState.Cancelled); // Verify timer duration, but execute it immediately. - var timer = Assert.Single(scheduledTimers); - Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); - timer.action(); + var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests()); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval); + timer.ForceFire(); Assert.True(holding == HoldingState.Started); @@ -220,10 +221,8 @@ namespace Avalonia.Base.UnitTests.Input iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300)); AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(iSettingsMock.Object); - - var scheduledTimers = new List<(TimeSpan time, Action action)>(); - using var app = UnitTestApplication.Start(new TestServices( - threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + using var app = UnitTestApplication.Start(); Border border = new Border(); Gestures.SetIsHoldWithMouseEnabled(border, true); @@ -233,7 +232,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Started); + decorator.AddHandler(Gestures.HoldingEvent, (_, e) => raised = e.HoldingState == HoldingState.Started); _mouse.Down(border); Assert.False(raised); @@ -242,9 +241,9 @@ namespace Avalonia.Base.UnitTests.Input Assert.False(raised); // Verify timer duration, but execute it immediately. - var timer = Assert.Single(scheduledTimers); - Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); - timer.action(); + var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests()); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval); + timer.ForceFire(); Assert.False(raised); } @@ -257,10 +256,8 @@ namespace Avalonia.Base.UnitTests.Input iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300)); AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(iSettingsMock.Object); - - var scheduledTimers = new List<(TimeSpan time, Action action)>(); - using var app = UnitTestApplication.Start(new TestServices( - threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + using var app = UnitTestApplication.Start(); Border border = new Border(); Gestures.SetIsHoldWithMouseEnabled(border, true); @@ -270,7 +267,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed); + decorator.AddHandler(Gestures.HoldingEvent, (_, e) => raised = e.HoldingState == HoldingState.Completed); _mouse.Down(border); Assert.False(raised); @@ -279,9 +276,9 @@ namespace Avalonia.Base.UnitTests.Input Assert.False(raised); // Verify timer duration, but execute it immediately. - var timer = Assert.Single(scheduledTimers); - Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); - timer.action(); + var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests()); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval); + timer.ForceFire(); Assert.False(raised); } @@ -294,10 +291,8 @@ namespace Avalonia.Base.UnitTests.Input iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300)); AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(iSettingsMock.Object); - - var scheduledTimers = new List<(TimeSpan time, Action action)>(); - using var app = UnitTestApplication.Start(new TestServices( - threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + using var app = UnitTestApplication.Start(); Border border = new Border(); Gestures.SetIsHoldWithMouseEnabled(border, true); @@ -307,14 +302,14 @@ namespace Avalonia.Base.UnitTests.Input }; var cancelled = false; - decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled); + decorator.AddHandler(Gestures.HoldingEvent, (_, e) => cancelled = e.HoldingState == HoldingState.Cancelled); _mouse.Down(border); Assert.False(cancelled); - var timer = Assert.Single(scheduledTimers); - Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); - timer.action(); + var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests()); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval); + timer.ForceFire(); var secondMouse = new MouseTestHelper(); @@ -333,9 +328,7 @@ namespace Avalonia.Base.UnitTests.Input AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(iSettingsMock.Object); - var scheduledTimers = new List<(TimeSpan time, Action action)>(); - using var app = UnitTestApplication.Start(new TestServices( - threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + using var app = UnitTestApplication.Start(); Border border = new Border(); Gestures.SetIsHoldWithMouseEnabled(border, true); @@ -345,13 +338,13 @@ namespace Avalonia.Base.UnitTests.Input }; var cancelled = false; - decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled); + decorator.AddHandler(Gestures.HoldingEvent, (_, e) => cancelled = e.HoldingState == HoldingState.Cancelled); _mouse.Down(border); - var timer = Assert.Single(scheduledTimers); - Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); - timer.action(); + var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests()); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval); + timer.ForceFire(); _mouse.Move(border, position: new Point(3, 3)); @@ -371,9 +364,7 @@ namespace Avalonia.Base.UnitTests.Input AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(iSettingsMock.Object); - var scheduledTimers = new List<(TimeSpan time, Action action)>(); - using var app = UnitTestApplication.Start(new TestServices( - threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + using var app = UnitTestApplication.Start(); Border border = new Border(); Gestures.SetIsHoldWithMouseEnabled(border, true); @@ -383,31 +374,21 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed); + decorator.AddHandler(Gestures.HoldingEvent, (_, e) => raised = e.HoldingState == HoldingState.Completed); var secondMouse = new MouseTestHelper(); _mouse.Down(border, MouseButton.Left); // Verify timer duration, but execute it immediately. - var timer = Assert.Single(scheduledTimers); - Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); - timer.action(); + var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests()); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval); + timer.ForceFire(); secondMouse.Down(border, MouseButton.Left); Assert.False(raised); } - - private static IPlatformThreadingInterface CreatePlatformThreadingInterface(Action<(TimeSpan, Action)> callback) - { - var threadingInterface = new Mock(); - threadingInterface.SetupGet(p => p.CurrentThreadIsLoopThread).Returns(true); - threadingInterface.Setup(p => p - .StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((_, t, a) => callback((t, a))); - return threadingInterface.Object; - } private static void AddHandlers( Decorator decorator, @@ -415,7 +396,7 @@ namespace Avalonia.Base.UnitTests.Input IList result, bool markHandled) { - decorator.AddHandler(Border.PointerPressedEvent, (s, e) => + decorator.AddHandler(InputElement.PointerPressedEvent, (_, e) => { result.Add("dp"); @@ -425,7 +406,7 @@ namespace Avalonia.Base.UnitTests.Input } }); - decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => + decorator.AddHandler(InputElement.PointerReleasedEvent, (_, e) => { result.Add("dr"); @@ -435,13 +416,13 @@ namespace Avalonia.Base.UnitTests.Input } }); - border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); - border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + border.AddHandler(InputElement.PointerPressedEvent, (_, _) => result.Add("bp")); + border.AddHandler(InputElement.PointerReleasedEvent, (_, _) => result.Add("br")); - decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); - decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); - border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); + decorator.AddHandler(Gestures.TappedEvent, (_, _) => result.Add("dt")); + decorator.AddHandler(Gestures.DoubleTappedEvent, (_, _) => result.Add("ddt")); + border.AddHandler(Gestures.TappedEvent, (_, _) => result.Add("bt")); + border.AddHandler(Gestures.DoubleTappedEvent, (_, _) => result.Add("bdt")); } [Fact] @@ -462,7 +443,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.PinchEvent, (s, e) => raised = true); + decorator.AddHandler(Gestures.PinchEvent, (_, _) => raised = true); var firstPoint = new Point(5, 5); var secondPoint = new Point(10, 10); @@ -490,7 +471,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.PinchEvent, (s, e) => raised = true); + decorator.AddHandler(Gestures.PinchEvent, (_, _) => raised = true); var firstPoint = new Point(5, 5); var secondPoint = new Point(10, 10); @@ -526,7 +507,7 @@ namespace Avalonia.Base.UnitTests.Input }; var raised = false; - decorator.AddHandler(Gestures.ScrollGestureEvent, (s, e) => raised = true); + decorator.AddHandler(Gestures.ScrollGestureEvent, (_, _) => raised = true); var firstTouch = new TouchTestHelper(); diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index c268fed28c..74e506105d 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -4,6 +4,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; +using Avalonia.Utilities; using Avalonia.VisualTree; using Moq; using Xunit; @@ -157,24 +158,10 @@ namespace Avalonia.Controls.UnitTests } } - [Fact(Skip = "Timers should NOT, in fact, be checked via IPlatformThreadingInterface")] + [Fact] public void Should_Open_On_Pointer_Enter_With_Delay() { - Action timercallback = null; - var delay = TimeSpan.Zero; - - var pti = Mock.Of(x => x.CurrentThreadIsLoopThread == true); - - Mock.Get(pti) - .Setup(v => v.StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((priority, interval, tick) => - { - delay = interval; - timercallback = tick; - }) - .Returns(Disposable.Empty); - - using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti))) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { var window = new Window(); @@ -194,11 +181,11 @@ namespace Avalonia.Controls.UnitTests _mouseHelper.Enter(target); - Assert.Equal(TimeSpan.FromMilliseconds(1), delay); - Assert.NotNull(timercallback); + var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests()); + Assert.Equal(TimeSpan.FromMilliseconds(1), timer.Interval); Assert.False(ToolTip.GetIsOpen(target)); - timercallback(); + timer.ForceFire(); Assert.True(ToolTip.GetIsOpen(target)); } From bc3768c2a06edf458ba8c797f1ee96372f68c59d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 17 Mar 2023 19:18:18 +0600 Subject: [PATCH 03/17] More platforms are now using IDispatcherImpl --- samples/ControlCatalog/App.xaml | 4 +- .../Threading/IDispatcherImpl.cs | 9 +- .../InternalPlatformThreadingInterface.cs | 92 --------------- .../Platform/ManagedDispatcherImpl.cs | 108 ++++++++++++++++++ .../Remote/PreviewerWindowingPlatform.cs | 4 +- .../AvaloniaHeadlessPlatform.cs | 2 +- .../HeadlessPlatformThreadingInterface.cs | 86 -------------- .../FramebufferToplevelImpl.cs | 9 +- .../Input/EvDev/EvDevBackend.cs | 11 +- .../Input/LibInput/LibInputBackend.cs | 4 +- .../LinuxFramebufferPlatform.cs | 12 +- src/Shared/RawEventGrouping.cs | 14 +++ tests/Avalonia.RenderTests/TestBase.cs | 4 +- 13 files changed, 149 insertions(+), 210 deletions(-) delete mode 100644 src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs create mode 100644 src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs delete mode 100644 src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 3b847adcbb..2c2e1c4075 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -60,7 +60,7 @@ - + diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 5d83ced011..089f5cb660 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -4,11 +4,8 @@ using Avalonia.Platform; namespace Avalonia.Threading; -interface IDispatcherImpl +public interface IDispatcherImpl { - - //IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick); - bool CurrentThreadIsLoopThread { get; } // Asynchronously triggers Signaled callback @@ -19,7 +16,7 @@ interface IDispatcherImpl } -interface IDispatcherImplWithPendingInput : IDispatcherImpl +public interface IDispatcherImplWithPendingInput : IDispatcherImpl { // Checks if dispatcher implementation can bool CanQueryPendingInput { get; } @@ -27,7 +24,7 @@ interface IDispatcherImplWithPendingInput : IDispatcherImpl bool HasPendingInput { get; } } -interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput +public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput { // Runs the event loop void RunLoop(CancellationToken token); diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs deleted file mode 100644 index e1f6db9c60..0000000000 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading; -using Avalonia.Metadata; -using Avalonia.Platform; -using Avalonia.Threading; - -namespace Avalonia.Controls.Platform -{ - [Unstable] - public class InternalPlatformThreadingInterface : IPlatformThreadingInterface - { - public InternalPlatformThreadingInterface() - { - TlsCurrentThreadIsLoopThread = true; - } - - private readonly AutoResetEvent _signaled = new AutoResetEvent(false); - - - public void RunLoop(CancellationToken cancellationToken) - { - var handles = new[] { _signaled, cancellationToken.WaitHandle }; - - while (!cancellationToken.IsCancellationRequested) - { - Signaled?.Invoke(null); - WaitHandle.WaitAny(handles); - } - } - - - class TimerImpl : IDisposable - { - private readonly DispatcherPriority _priority; - private readonly TimeSpan _interval; - private readonly Action _tick; - private Timer? _timer; - private GCHandle _handle; - - public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick) - { - _priority = priority; - _interval = interval; - _tick = tick; - _timer = new Timer(OnTimer, null, interval, Timeout.InfiniteTimeSpan); - _handle = GCHandle.Alloc(_timer); - } - - private void OnTimer(object? state) - { - if (_timer == null) - return; - Dispatcher.UIThread.Post(() => - { - - if (_timer == null) - return; - _tick(); - _timer?.Change(_interval, Timeout.InfiniteTimeSpan); - }); - } - - - public void Dispose() - { - _handle.Free(); - _timer?.Dispose(); - _timer = null; - } - } - - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) - { - return new TimerImpl(priority, interval, tick); - } - - public void Signal(DispatcherPriority prio) - { - _signaled.Set(); - } - - [ThreadStatic] private static bool TlsCurrentThreadIsLoopThread; - - public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread; - public event Action? Signaled; -#pragma warning disable CS0067 - public event Action? Tick; -#pragma warning restore CS0067 - - } -} diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs new file mode 100644 index 0000000000..bc7595ef4e --- /dev/null +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -0,0 +1,108 @@ +using System; +using System.Diagnostics; +using System.Threading; +using Avalonia.Metadata; +using Avalonia.Threading; + +namespace Avalonia.Controls.Platform; + +[Unstable] +public class ManagedDispatcherImpl : IControlledDispatcherImpl +{ + private readonly IManagedDispatcherInputProvider? _inputProvider; + private readonly AutoResetEvent _wakeup = new(false); + private bool _signaled; + private readonly object _lock = new(); + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private TimeSpan? _nextTimer; + private readonly Thread _loopThread = Thread.CurrentThread; + + public interface IManagedDispatcherInputProvider + { + bool HasInput { get; } + void DispatchNextInputEvent(); + } + + public ManagedDispatcherImpl(IManagedDispatcherInputProvider? inputProvider) + { + _inputProvider = inputProvider; + } + + public bool CurrentThreadIsLoopThread => _loopThread == Thread.CurrentThread; + public void Signal() + { + lock (_lock) + { + _signaled = true; + _wakeup.Set(); + } + } + + public event Action? Signaled; + public event Action? Timer; + public void UpdateTimer(int? dueTimeInTicks) + { + lock (_lock) + { + _nextTimer = dueTimeInTicks == null + ? null + : _clock.Elapsed + TimeSpan.FromMilliseconds(dueTimeInTicks.Value); + if (!CurrentThreadIsLoopThread) + _wakeup.Set(); + } + } + + public bool CanQueryPendingInput => _inputProvider != null; + public bool HasPendingInput => _inputProvider?.HasInput ?? false; + + public void RunLoop(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + bool signaled; + lock (_lock) + { + signaled = _signaled; + _signaled = false; + } + + if (signaled) + { + Signaled?.Invoke(); + continue; + } + + bool fireTimer = false; + lock (_lock) + { + if (_nextTimer < _clock.Elapsed) + { + fireTimer = true; + _nextTimer = null; + } + } + + if (fireTimer) + { + Timer?.Invoke(); + continue; + } + + if (_inputProvider?.HasInput == true) + { + _inputProvider.DispatchNextInputEvent(); + continue; + } + + if (_nextTimer != null) + { + var waitFor = _clock.Elapsed - _nextTimer.Value; + if (waitFor.TotalMilliseconds < 1) + continue; + _wakeup.WaitOne(waitFor); + } + else + _wakeup.WaitOne(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index cd14b2d69a..808153cc4a 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -6,6 +6,7 @@ using Avalonia.Input.Platform; using Avalonia.Platform; using Avalonia.Remote.Protocol; using Avalonia.Rendering; +using Avalonia.Threading; namespace Avalonia.DesignerSupport.Remote { @@ -46,13 +47,12 @@ namespace Avalonia.DesignerSupport.Remote { s_transport = transport; var instance = new PreviewerWindowingPlatform(); - var threading = new InternalPlatformThreadingInterface(); AvaloniaLocator.CurrentMutable .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(Keyboard) .Bind().ToSingleton() - .Bind().ToConstant(threading) + .Bind().ToConstant(new ManagedDispatcherImpl(null)) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(instance) diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b0b1d731d2..319b0da7bf 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -60,7 +60,7 @@ namespace Avalonia.Headless internal static void Initialize(AvaloniaHeadlessPlatformOptions opts) { AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new HeadlessPlatformThreadingInterface()) + .Bind().ToConstant(new ManagedDispatcherImpl(null)) .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToSingleton() diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs deleted file mode 100644 index 046e4645e3..0000000000 --- a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using Avalonia.Reactive; -using System.Threading; -using Avalonia.Platform; -using Avalonia.Threading; - -namespace Avalonia.Headless -{ - class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface - { - public HeadlessPlatformThreadingInterface() - { - _thread = Thread.CurrentThread; - } - - private AutoResetEvent _event = new AutoResetEvent(false); - private Thread _thread; - private object _lock = new object(); - private DispatcherPriority? _signaledPriority; - - public void RunLoop(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - DispatcherPriority? signaled = null; - lock (_lock) - { - signaled = _signaledPriority; - _signaledPriority = null; - } - if(signaled.HasValue) - Signaled?.Invoke(signaled); - WaitHandle.WaitAny(new[] {cancellationToken.WaitHandle, _event}, TimeSpan.FromMilliseconds(20)); - } - } - - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) - { - if (interval.TotalMilliseconds < 10) - interval = TimeSpan.FromMilliseconds(10); - - var stopped = false; - Timer timer = null; - timer = new Timer(_ => - { - if (stopped) - return; - - Dispatcher.UIThread.Post(() => - { - try - { - tick(); - } - finally - { - if (!stopped) - timer.Change(interval, Timeout.InfiniteTimeSpan); - } - }); - }, - null, interval, Timeout.InfiniteTimeSpan); - - return Disposable.Create(() => - { - stopped = true; - timer.Dispose(); - }); - } - - public void Signal(DispatcherPriority priority) - { - lock (_lock) - { - if (_signaledPriority == null || _signaledPriority.Value > priority) - { - _signaledPriority = priority; - } - _event.Set(); - } - } - - public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread; - public event Action Signaled; - } -} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 9dc7f08064..af4a70f128 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -8,13 +8,15 @@ using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; + using Avalonia.Threading; -namespace Avalonia.LinuxFramebuffer + namespace Avalonia.LinuxFramebuffer { class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider { private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; + private readonly RawEventGrouper _inputQueue; public IInputRoot InputRoot { get; private set; } @@ -22,9 +24,12 @@ namespace Avalonia.LinuxFramebuffer { _outputBackend = outputBackend; _inputBackend = inputBackend; + _inputQueue = new RawEventGrouper(groupedInput => Input?.Invoke(groupedInput), + LinuxFramebufferPlatform.EventGrouperDispatchQueue); Surfaces = new object[] { _outputBackend }; - _inputBackend.Initialize(this, e => Input?.Invoke(e)); + _inputBackend.Initialize(this, e => + Dispatcher.UIThread.Post(() => _inputQueue.HandleEvent(e), DispatcherPriority.Send )); } public IRenderer CreateRenderer(IRenderRoot root) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs index 686050e7c2..61c94a48bd 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs @@ -14,12 +14,10 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev private int _epoll; private Action _onInput; private IInputRoot _inputRoot; - private RawEventGroupingThreadingHelper _inputQueue; public EvDevBackend(EvDevDeviceDescription[] devices) { _deviceDescriptions = devices; - _inputQueue = new RawEventGroupingThreadingHelper(e => _onInput?.Invoke(e)); } unsafe void InputThread() @@ -45,12 +43,9 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev } } - private void OnRawEvent(RawInputEventArgs obj) - { - _inputQueue.OnEvent(obj); - } - - + private void OnRawEvent(RawInputEventArgs obj) => _onInput?.Invoke(obj); + + public void Initialize(IScreenInfoProvider info, Action onInput) { _onInput = onInput; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index bff9ddc55c..300cbc2689 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -13,14 +13,12 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput private IInputRoot _inputRoot; private TouchDevice _touch = new TouchDevice(); private const string LibInput = nameof(Avalonia.LinuxFramebuffer) + "/" + nameof(Avalonia.LinuxFramebuffer.Input) + "/" + nameof(LibInput); - private readonly RawEventGroupingThreadingHelper _inputQueue; private Action _onInput; private Dictionary _pointers = new Dictionary(); public LibInputBackend() { var ctx = libinput_path_create_context(); - _inputQueue = new(e => _onInput?.Invoke(e)); new Thread(() => InputThread(ctx)).Start(); } @@ -58,7 +56,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput } } - private void ScheduleInput(RawInputEventArgs ev) => _inputQueue.OnEvent(ev); + private void ScheduleInput(RawInputEventArgs ev) => _onInput.Invoke(ev); private void HandleTouch(IntPtr ev, LibInputEventType type) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index c3e90f5fd7..bc178c8ecb 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -16,6 +16,8 @@ using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +using Avalonia.Threading; + #nullable enable namespace Avalonia.LinuxFramebuffer @@ -23,9 +25,7 @@ namespace Avalonia.LinuxFramebuffer class LinuxFramebufferPlatform { IOutputBackend _fb; - private static readonly Stopwatch St = Stopwatch.StartNew(); - internal static uint Timestamp => (uint)St.ElapsedTicks; - public static InternalPlatformThreadingInterface? Threading; + public static ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue = new(); internal static Compositor Compositor { get; private set; } = null!; @@ -34,18 +34,16 @@ namespace Avalonia.LinuxFramebuffer { _fb = backend; } - - + void Initialize() { - Threading = new InternalPlatformThreadingInterface(); if (_fb is IGlOutputBackend gl) AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformGraphics); var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(Threading) + .Bind().ToConstant(new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))) .Bind().ToConstant(new DefaultRenderTimer(opts.Fps)) .Bind().ToConstant(new RenderLoop()) .Bind().ToTransient() diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs index 66b24e9722..cdcd5af142 100644 --- a/src/Shared/RawEventGrouping.cs +++ b/src/Shared/RawEventGrouping.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Collections.Pooled; +using Avalonia.Controls.Platform; using Avalonia.Input.Raw; using Avalonia.Threading; @@ -34,6 +35,19 @@ class ManualRawEventGrouperDispatchQueue : IRawEventGrouperDispatchQueue } } +internal class ManualRawEventGrouperDispatchQueueDispatcherInputProvider : ManagedDispatcherImpl.IManagedDispatcherInputProvider +{ + private readonly ManualRawEventGrouperDispatchQueue _queue; + + public ManualRawEventGrouperDispatchQueueDispatcherInputProvider(ManualRawEventGrouperDispatchQueue queue) + { + _queue = queue; + } + + public bool HasInput => _queue.HasJobs; + public void DispatchNextInputEvent() => _queue.DispatchNext(); +} + internal class AutomaticRawEventGrouperDispatchQueue : IRawEventGrouperDispatchQueue { private readonly Queue<(RawInputEventArgs args, Action handler)> _inputQueue = new(); diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 81474d5efb..fd258dfd96 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -17,6 +17,7 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.Threading; +using Avalonia.Utilities; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; #if AVALONIA_SKIA @@ -122,7 +123,8 @@ namespace Avalonia.Direct2D1.RenderTests // Free pools for (var c = 0; c < 11; c++) - TestThreadingInterface.RunTimers(); + foreach (var dp in Dispatcher.SnapshotTimersForUnitTests()) + dp.ForceFire(); writableBitmap.Save(compositedPath); } } From 1cdb876847ab50cfdae9fe4aaf565869632fd3c7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 17 Mar 2023 19:51:05 +0600 Subject: [PATCH 04/17] Revert SynchronizationContext::Post --- src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index b8ac83f418..1b29cf32f7 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -30,7 +30,7 @@ namespace Avalonia.Threading /// public override void Post(SendOrPostCallback d, object? state) { - Dispatcher.UIThread.Post(() => d(state), DispatcherPriority.Background); + Dispatcher.UIThread.Post(d, state, DispatcherPriority.Background); } /// From ccc0efaacde4740fcf86c7240565e4ed5ad4cecd Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 17 Mar 2023 21:36:12 +0600 Subject: [PATCH 05/17] Fixed DispatcherTimerHelper for unit tests --- src/Avalonia.Base/Threading/Dispatcher.Queue.cs | 5 ++++- src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 0ce2479a45..eedfe12734 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -16,7 +16,10 @@ public partial class Dispatcher { _backgroundTimer = new DispatcherTimer(this, DispatcherPriority.Send, - TimeSpan.FromMilliseconds(1)); + TimeSpan.FromMilliseconds(1)) + { + Tag = "Dispatcher.RequestBackgroundProcessing" + }; _backgroundTimer.Tick += delegate { _backgroundTimer.Stop(); diff --git a/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs b/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs index 0c4a5f1051..a457388fb2 100644 --- a/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs +++ b/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs @@ -12,6 +12,7 @@ public static class DispatcherTimerUtils public static void ForceFire(this DispatcherTimer timer) { timer.Promote(); + timer.Dispatcher.RemoveTimer(timer); timer.Dispatcher.RunJobs(); } } \ No newline at end of file From 763c53c2c78ced844f18cbf433208697b7372cb1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 17 Mar 2023 22:07:32 +0300 Subject: [PATCH 06/17] Allow sync wait for non-UI threads --- src/Avalonia.Base/Threading/DispatcherOperation.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index b0d7e5a049..173ab81ef8 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -253,10 +253,9 @@ public class DispatcherOperation : DispatcherOperation { get { - if (TaskCompletionSource.Task.IsCompleted) + if (TaskCompletionSource.Task.IsCompleted || !Dispatcher.CheckAccess()) return TaskCompletionSource.Task.GetAwaiter().GetResult(); throw new InvalidOperationException("Synchronous wait is only supported on non-UI threads"); - } } } @@ -306,4 +305,4 @@ public enum DispatcherOperationStatus Aborted = 1, Completed = 2, Executing = 3, -} \ No newline at end of file +} From 067df73f9708d5078038647d2273a1e190e44db5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 18 Mar 2023 16:26:37 +0600 Subject: [PATCH 07/17] Naming --- src/Avalonia.Base/Threading/Dispatcher.Queue.cs | 2 +- src/Avalonia.Base/Threading/Dispatcher.Timers.cs | 16 ++++++++-------- src/Avalonia.Base/Threading/IDispatcherImpl.cs | 10 +++++----- .../Platform/ManagedDispatcherImpl.cs | 6 +++--- src/Avalonia.Native/DispatcherImpl.cs | 4 ++-- src/Avalonia.X11/X11PlatformThreading.cs | 4 ++-- .../Avalonia.Win32/Win32DispatcherImpl.cs | 6 +++--- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index eedfe12734..1550da079f 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -59,7 +59,7 @@ public partial class Dispatcher public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { } } diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index e71e7375d3..0f9924d90d 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -9,7 +9,7 @@ public partial class Dispatcher private List _timers = new(); private long _timersVersion; private bool _dueTimeFound; - private int _dueTimeInTicks; + private int _dueTimeInMs; private bool _isOsTimerSet; internal void UpdateOSTimer() @@ -25,9 +25,9 @@ public partial class Dispatcher if (!_hasShutdownFinished) // Dispatcher thread, does not technically need the lock to read { bool oldDueTimeFound = _dueTimeFound; - int oldDueTimeInTicks = _dueTimeInTicks; + int oldDueTimeInTicks = _dueTimeInMs; _dueTimeFound = false; - _dueTimeInTicks = 0; + _dueTimeInMs = 0; if (_timers.Count > 0) { @@ -36,19 +36,19 @@ public partial class Dispatcher { var timer = _timers[i]; - if (!_dueTimeFound || timer.DueTimeInMs - _dueTimeInTicks < 0) + if (!_dueTimeFound || timer.DueTimeInMs - _dueTimeInMs < 0) { _dueTimeFound = true; - _dueTimeInTicks = timer.DueTimeInMs; + _dueTimeInMs = timer.DueTimeInMs; } } } if (_dueTimeFound) { - if (!_isOsTimerSet || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInTicks)) + if (!_isOsTimerSet || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInMs)) { - _impl.UpdateTimer(Math.Max(1, _dueTimeInTicks)); + _impl.UpdateTimer(Math.Max(1, _dueTimeInMs)); _isOsTimerSet = true; } } @@ -111,7 +111,7 @@ public partial class Dispatcher { if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read { - if (_dueTimeFound && _dueTimeInTicks - currentTimeInTicks <= 0) + if (_dueTimeFound && _dueTimeInMs - currentTimeInTicks <= 0) { timers = _timers; timersVersion = _timersVersion; diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 089f5cb660..ab501f698c 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -12,7 +12,7 @@ public interface IDispatcherImpl void Signal(); event Action Signaled; event Action Timer; - void UpdateTimer(int? dueTimeInTicks); + void UpdateTimer(int? dueTimeInMs); } @@ -46,13 +46,13 @@ internal class LegacyDispatcherImpl : IControlledDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { _timer?.Dispose(); _timer = null; - if (dueTimeInTicks.HasValue) + if (dueTimeInMs.HasValue) _timer = _platformThreading.StartTimer(DispatcherPriority.Send, - TimeSpan.FromMilliseconds(dueTimeInTicks.Value), + TimeSpan.FromMilliseconds(dueTimeInMs.Value), OnTick); } @@ -80,7 +80,7 @@ class NullDispatcherImpl : IDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { } diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index bc7595ef4e..7a487e99fb 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -40,13 +40,13 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { lock (_lock) { - _nextTimer = dueTimeInTicks == null + _nextTimer = dueTimeInMs == null ? null - : _clock.Elapsed + TimeSpan.FromMilliseconds(dueTimeInTicks.Value); + : _clock.Elapsed + TimeSpan.FromMilliseconds(dueTimeInMs.Value); if (!CurrentThreadIsLoopThread) _wakeup.Set(); } diff --git a/src/Avalonia.Native/DispatcherImpl.cs b/src/Avalonia.Native/DispatcherImpl.cs index 4788dd5e82..a9e5e6deb1 100644 --- a/src/Avalonia.Native/DispatcherImpl.cs +++ b/src/Avalonia.Native/DispatcherImpl.cs @@ -54,9 +54,9 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public void Signal() => _native.Signal(); - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { - var ms = dueTimeInTicks == null ? -1 : Math.Max(1, dueTimeInTicks.Value - TickCount); + var ms = dueTimeInMs == null ? -1 : Math.Max(1, dueTimeInMs.Value - TickCount); _native.UpdateTimer(ms); } diff --git a/src/Avalonia.X11/X11PlatformThreading.cs b/src/Avalonia.X11/X11PlatformThreading.cs index b8a5b68658..f2f45bce8e 100644 --- a/src/Avalonia.X11/X11PlatformThreading.cs +++ b/src/Avalonia.X11/X11PlatformThreading.cs @@ -227,9 +227,9 @@ namespace Avalonia.X11 public event Action Signaled; public event Action Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { - _nextTimer = dueTimeInTicks; + _nextTimer = dueTimeInMs; if (_nextTimer != null) Wakeup(); } diff --git a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs index 0916490ed8..72a60902ba 100644 --- a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs +++ b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs @@ -39,14 +39,14 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock void TimerProc(IntPtr hWnd, uint uMsg, IntPtr nIdEvent, uint dwTime) => Timer?.Invoke(); - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { if (_timerHandle.HasValue) KillTimer(IntPtr.Zero, _timerHandle.Value); - if (dueTimeInTicks == null) + if (dueTimeInMs == null) return; - var interval = (uint)Math.Max(1, TickCount - dueTimeInTicks.Value); + var interval = (uint)Math.Max(1, TickCount - dueTimeInMs.Value); _timerHandle = SetTimer( IntPtr.Zero, From 718089bda93d2cf2e0bf063dfdab1968dc0c4d25 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 18 Mar 2023 16:35:09 +0600 Subject: [PATCH 08/17] Fixed some timers --- src/Avalonia.Base/Threading/IDispatcherImpl.cs | 8 ++++++-- src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index ab501f698c..7f1e352b51 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -30,7 +30,7 @@ public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput void RunLoop(CancellationToken token); } -internal class LegacyDispatcherImpl : IControlledDispatcherImpl +internal class LegacyDispatcherImpl : DefaultDispatcherClock, IControlledDispatcherImpl { private readonly IPlatformThreadingInterface _platformThreading; private IDisposable? _timer; @@ -50,10 +50,14 @@ internal class LegacyDispatcherImpl : IControlledDispatcherImpl { _timer?.Dispose(); _timer = null; + if (dueTimeInMs.HasValue) + { + var interval = Math.Max(1, dueTimeInMs.Value - TickCount); _timer = _platformThreading.StartTimer(DispatcherPriority.Send, - TimeSpan.FromMilliseconds(dueTimeInMs.Value), + TimeSpan.FromMilliseconds(interval), OnTick); + } } private void OnTick() diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index 7a487e99fb..54c96113ea 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -46,7 +46,7 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl { _nextTimer = dueTimeInMs == null ? null - : _clock.Elapsed + TimeSpan.FromMilliseconds(dueTimeInMs.Value); + : TimeSpan.FromMilliseconds(dueTimeInMs.Value); if (!CurrentThreadIsLoopThread) _wakeup.Set(); } From d96b8124ed63c72a9ca176fb17d4a1dc8b09e808 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 18 Mar 2023 17:03:04 +0600 Subject: [PATCH 09/17] Don't use DispatcherTimer infrastructure for background processing --- .../Threading/Dispatcher.Queue.cs | 31 +++++----- .../Threading/Dispatcher.Timers.cs | 59 ++++++++++++++----- .../Threading/DispatcherTimer.cs | 2 +- .../DispatcherTests.cs | 25 +++----- 4 files changed, 71 insertions(+), 46 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 1550da079f..4360d8111d 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -7,26 +7,18 @@ public partial class Dispatcher { private readonly DispatcherPriorityQueue _queue = new(); private bool _signaled; - private DispatcherTimer? _backgroundTimer; private const int MaximumTimeProcessingBackgroundJobs = 50; void RequestBackgroundProcessing() { - if (_backgroundTimer == null) + lock (InstanceLock) { - _backgroundTimer = - new DispatcherTimer(this, DispatcherPriority.Send, - TimeSpan.FromMilliseconds(1)) - { - Tag = "Dispatcher.RequestBackgroundProcessing" - }; - _backgroundTimer.Tick += delegate + if (_dueTimeForBackgroundProcessing == null) { - _backgroundTimer.Stop(); - }; + _dueTimeForBackgroundProcessing = Clock.TickCount + 1; + UpdateOSTimer(); + } } - - _backgroundTimer.IsEnabled = true; } /// @@ -167,8 +159,19 @@ public partial class Dispatcher { lock (InstanceLock) { + if (!CheckAccess()) + { + RequestForegroundProcessing(); + return true; + } + if (_queue.MaxPriority <= DispatcherPriority.Input) - RequestBackgroundProcessing(); + { + if (_pendingInputImpl is { CanQueryPendingInput: true, HasPendingInput: false }) + RequestForegroundProcessing(); + else + RequestBackgroundProcessing(); + } else RequestForegroundProcessing(); } diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index 0f9924d90d..0bf087918e 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -10,13 +10,30 @@ public partial class Dispatcher private long _timersVersion; private bool _dueTimeFound; private int _dueTimeInMs; - private bool _isOsTimerSet; - internal void UpdateOSTimer() + private int? _dueTimeForTimers; + private int? _dueTimeForBackgroundProcessing; + private int? _osTimerSetTo; + + private void UpdateOSTimer() + { + lock (InstanceLock) + { + var nextDueTime = + (_dueTimeForTimers.HasValue && _dueTimeForBackgroundProcessing.HasValue) + ? Math.Min(_dueTimeForTimers.Value, _dueTimeForBackgroundProcessing.Value) + : _dueTimeForTimers ?? _dueTimeForBackgroundProcessing; + if(_osTimerSetTo == nextDueTime) + return; + _impl.UpdateTimer(_osTimerSetTo = nextDueTime); + } + } + + internal void UpdateOSTimerForTimers() { if (!CheckAccess()) { - Post(UpdateOSTimer, DispatcherPriority.Send); + Post(UpdateOSTimerForTimers, DispatcherPriority.Send); return; } @@ -46,16 +63,16 @@ public partial class Dispatcher if (_dueTimeFound) { - if (!_isOsTimerSet || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInMs)) + if (_dueTimeForTimers == null || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInMs)) { - _impl.UpdateTimer(Math.Max(1, _dueTimeInMs)); - _isOsTimerSet = true; + _dueTimeForTimers = _dueTimeInMs; + UpdateOSTimer(); } } else if (oldDueTimeFound) { - _impl.UpdateTimer(null); - _isOsTimerSet = false; + _dueTimeForTimers = null; + UpdateOSTimer(); } } } @@ -72,7 +89,7 @@ public partial class Dispatcher } } - UpdateOSTimer(); + UpdateOSTimerForTimers(); } internal void RemoveTimer(DispatcherTimer timer) @@ -86,17 +103,29 @@ public partial class Dispatcher } } - UpdateOSTimer(); + UpdateOSTimerForTimers(); } private void OnOSTimer() { + bool needToPromoteTimers = false; + bool needToProcessQueue = false; lock (InstanceLock) { - _impl.UpdateTimer(null); - _isOsTimerSet = false; + needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Clock.TickCount; + if (needToPromoteTimers) + _dueTimeForTimers = null; + needToProcessQueue = _dueTimeForBackgroundProcessing.HasValue && + _dueTimeForBackgroundProcessing.Value <= Clock.TickCount; + if (needToProcessQueue) + _dueTimeForBackgroundProcessing = null; + UpdateOSTimer(); } - PromoteTimers(); + + if (needToPromoteTimers) + PromoteTimers(); + if (needToProcessQueue) + ExecuteJobsCore(); } internal void PromoteTimers() @@ -166,10 +195,10 @@ public partial class Dispatcher } finally { - UpdateOSTimer(); + UpdateOSTimerForTimers(); } } internal static List SnapshotTimersForUnitTests() => - s_uiThread!._timers.Where(t => t != s_uiThread._backgroundTimer).ToList(); + s_uiThread!._timers.ToList(); } \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 183f66eb61..f7ed03379e 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -132,7 +132,7 @@ public partial class DispatcherTimer if (updateOSTimer) { - _dispatcher.UpdateOSTimer(); + _dispatcher.UpdateOSTimerForTimers(); } } } diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index 450c857cfe..902af94121 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -99,15 +99,12 @@ public class DispatcherTests Assert.False(impl.AskedForSignal); Assert.NotNull(impl.NextTimer); - impl.ExecuteTimer(); - Assert.True(impl.AskedForSignal); - Assert.Null(impl.NextTimer); - for (var c = 0; c < 4; c++) { - if (impl.NextTimer != null) - impl.ExecuteTimer(); - Assert.True(impl.AskedForSignal); + Assert.NotNull(impl.NextTimer); + Assert.False(impl.AskedForSignal); + impl.ExecuteTimer(); + Assert.False(impl.AskedForSignal); impl.ExecuteSignal(); var expectedCount = (c + 1) * 3; if (c == 3) @@ -129,7 +126,7 @@ public class DispatcherTests public void DispatcherStopsItemProcessingWhenInputIsPending() { var impl = new SimpleDispatcherImpl(); - impl.TestInputPending = false; + impl.TestInputPending = true; var disp = new Dispatcher(impl, impl); var actions = new List(); for (var c = 0; c < 10; c++) @@ -144,17 +141,13 @@ public class DispatcherTests } Assert.False(impl.AskedForSignal); Assert.NotNull(impl.NextTimer); - - impl.ExecuteTimer(); - Assert.True(impl.AskedForSignal); - Assert.Null(impl.NextTimer); + impl.TestInputPending = false; for (var c = 0; c < 4; c++) { - if (impl.NextTimer != null) - impl.ExecuteTimer(); - Assert.True(impl.AskedForSignal); - impl.ExecuteSignal(); + Assert.NotNull(impl.NextTimer); + impl.ExecuteTimer(); + Assert.False(impl.AskedForSignal); var expectedCount = c switch { 0 => 1, From fe4945d395f4a119a340decf8fc311f98c7bcfd2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 18 Mar 2023 18:55:00 +0300 Subject: [PATCH 10/17] Win32 dispatcher fixes --- .../Threading/Dispatcher.Timers.cs | 35 ++++++++++--------- .../Threading/DispatcherTimer.cs | 2 +- .../Interop/UnmanagedMethods.cs | 2 +- .../Avalonia.Win32/Win32DispatcherImpl.cs | 30 ++++++++-------- src/Windows/Avalonia.Win32/Win32Platform.cs | 29 +++++---------- 5 files changed, 42 insertions(+), 56 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index 0bf087918e..269d10707e 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -17,23 +17,21 @@ public partial class Dispatcher private void UpdateOSTimer() { - lock (InstanceLock) - { - var nextDueTime = - (_dueTimeForTimers.HasValue && _dueTimeForBackgroundProcessing.HasValue) - ? Math.Min(_dueTimeForTimers.Value, _dueTimeForBackgroundProcessing.Value) - : _dueTimeForTimers ?? _dueTimeForBackgroundProcessing; - if(_osTimerSetTo == nextDueTime) - return; - _impl.UpdateTimer(_osTimerSetTo = nextDueTime); - } + VerifyAccess(); + var nextDueTime = + (_dueTimeForTimers.HasValue && _dueTimeForBackgroundProcessing.HasValue) ? + Math.Min(_dueTimeForTimers.Value, _dueTimeForBackgroundProcessing.Value) : + _dueTimeForTimers ?? _dueTimeForBackgroundProcessing; + if (_osTimerSetTo == nextDueTime) + return; + _impl.UpdateTimer(_osTimerSetTo = nextDueTime); } - internal void UpdateOSTimerForTimers() + internal void RescheduleTimers() { if (!CheckAccess()) { - Post(UpdateOSTimerForTimers, DispatcherPriority.Send); + Post(RescheduleTimers, DispatcherPriority.Send); return; } @@ -89,7 +87,7 @@ public partial class Dispatcher } } - UpdateOSTimerForTimers(); + RescheduleTimers(); } internal void RemoveTimer(DispatcherTimer timer) @@ -103,15 +101,18 @@ public partial class Dispatcher } } - UpdateOSTimerForTimers(); + RescheduleTimers(); } private void OnOSTimer() { + _impl.UpdateTimer(null); + _osTimerSetTo = null; bool needToPromoteTimers = false; bool needToProcessQueue = false; lock (InstanceLock) { + _impl.UpdateTimer(_osTimerSetTo = null); needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Clock.TickCount; if (needToPromoteTimers) _dueTimeForTimers = null; @@ -119,13 +120,13 @@ public partial class Dispatcher _dueTimeForBackgroundProcessing.Value <= Clock.TickCount; if (needToProcessQueue) _dueTimeForBackgroundProcessing = null; - UpdateOSTimer(); } if (needToPromoteTimers) PromoteTimers(); if (needToProcessQueue) ExecuteJobsCore(); + UpdateOSTimer(); } internal void PromoteTimers() @@ -195,10 +196,10 @@ public partial class Dispatcher } finally { - UpdateOSTimerForTimers(); + RescheduleTimers(); } } internal static List SnapshotTimersForUnitTests() => s_uiThread!._timers.ToList(); -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index f7ed03379e..0c235ee161 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -132,7 +132,7 @@ public partial class DispatcherTimer if (updateOSTimer) { - _dispatcher.UpdateOSTimerForTimers(); + _dispatcher.RescheduleTimers(); } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 9618a5b4cd..7510b48270 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1796,7 +1796,7 @@ namespace Avalonia.Win32.Interop QS_SENDMESSAGE = 0x0040, QS_HOTKEY = 0x0080, QS_ALLPOSTMESSAGE = 0x0100, - QS_EVENT = 0x0200, + QS_EVENT = 0x02000, QS_MOUSE = QS_MOUSEMOVE | QS_MOUSEBUTTON, QS_INPUT = QS_MOUSE | QS_KEY, QS_ALLEVENTS = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY, diff --git a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs index 72a60902ba..3c2f7842ba 100644 --- a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs +++ b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs @@ -10,13 +10,10 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock { private readonly IntPtr _messageWindow; private static Thread? s_uiThread; - private IntPtr? _timerHandle; - private readonly TimerProc _timerDelegate; public Win32DispatcherImpl(IntPtr messageWindow) { _messageWindow = messageWindow; s_uiThread = Thread.CurrentThread; - _timerDelegate = TimerProc; } public bool CurrentThreadIsLoopThread => s_uiThread == Thread.CurrentThread; @@ -37,26 +34,27 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public event Action? Signaled; public event Action? Timer; - void TimerProc(IntPtr hWnd, uint uMsg, IntPtr nIdEvent, uint dwTime) => Timer?.Invoke(); + public void FireTimer() => Timer?.Invoke(); public void UpdateTimer(int? dueTimeInMs) { - if (_timerHandle.HasValue) - KillTimer(IntPtr.Zero, _timerHandle.Value); if (dueTimeInMs == null) - return; - - var interval = (uint)Math.Max(1, TickCount - dueTimeInMs.Value); - - _timerHandle = SetTimer( - IntPtr.Zero, - IntPtr.Zero, - interval, - _timerDelegate); + { + KillTimer(_messageWindow, (IntPtr)Win32Platform.TIMERID_DISPATCHER); + } + else + { + var interval = (uint)Math.Max(1, TickCount - dueTimeInMs.Value); + SetTimer( + _messageWindow, + (IntPtr)Win32Platform.TIMERID_DISPATCHER, + interval, + null!); + } } public bool CanQueryPendingInput => true; - + public bool HasPendingInput { get diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 6a5841f36f..857af3a783 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -112,6 +112,7 @@ namespace Avalonia.Win32 private static readonly Win32Platform s_instance = new(); private static Win32PlatformOptions? s_options; private static Compositor? s_compositor; + internal const int TIMERID_DISPATCHER = 1; private WndProc? _wndProcDelegate; private IntPtr _hwnd; @@ -182,27 +183,7 @@ namespace Avalonia.Win32 s_compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), platformGraphics); } - - public bool HasMessages() - { - return PeekMessage(out _, IntPtr.Zero, 0, 0, 0); - } - - public void ProcessMessage() - { - if (GetMessage(out var msg, IntPtr.Zero, 0, 0) > -1) - { - TranslateMessage(ref msg); - DispatchMessage(ref msg); - } - else - { - Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform) - ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(ProcessMessage), Marshal.GetLastWin32Error()); - - } - } - + public event EventHandler? ShutdownRequested; [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] @@ -238,6 +219,12 @@ namespace Avalonia.Win32 win32PlatformSettings.OnColorValuesChanged(); } } + + if (msg == (uint)WindowsMessage.WM_TIMER) + { + if (wParam == (IntPtr)TIMERID_DISPATCHER) + _dispatcher?.FireTimer(); + } TrayIconImpl.ProcWnd(hWnd, msg, wParam, lParam); From 290ab36d84e549b292d58849e4548c24ecee6114 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 19 Mar 2023 14:26:47 +0300 Subject: [PATCH 11/17] Reset the signaled state before executing jobs --- src/Avalonia.Base/Threading/Dispatcher.Queue.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 4360d8111d..9db8c5e8c0 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -94,15 +94,10 @@ public partial class Dispatcher private void Signaled() { - try - { - ExecuteJobsCore(); - } - finally - { - lock (InstanceLock) - _signaled = false; - } + lock (InstanceLock) + _signaled = false; + + ExecuteJobsCore(); } void ExecuteJobsCore() From ee445f21976e9d40166e59ebd2866e4fa2a9ff10 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 19 Mar 2023 15:54:35 +0600 Subject: [PATCH 12/17] Fixed dispatcher for macOS --- native/Avalonia.Native/src/OSX/platformthreading.mm | 4 +--- src/Avalonia.Base/Threading/Dispatcher.Queue.cs | 8 +++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index b727b9a6cf..d2d7a365a6 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -79,13 +79,11 @@ static double distantFutureInterval = (double)50*365*24*3600; bool signaled; @synchronized (self) { signaled = self->_signaled; + self->_signaled = false; } if(signaled) { self->_events->Signaled(); - @synchronized (self) { - self->_signaled = false; - } } }); CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 9db8c5e8c0..0c5414e6d1 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -124,7 +124,13 @@ public partial class Dispatcher else if (_pendingInputImpl?.CanQueryPendingInput == true) { if (!_pendingInputImpl.HasPendingInput) - ExecuteJob(job); + { + // On platforms like macOS HasPendingInput check might trigger a timer + // which would result in reentrancy here, so we check if the job + // hasn't been executed yet + if (job.Status == DispatcherOperationStatus.Pending) + ExecuteJob(job); + } else { RequestBackgroundProcessing(); From 13bbdc729e026a7c4e6ec8ce33f5d83c9f24648f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 19 Mar 2023 19:52:17 +0600 Subject: [PATCH 13/17] Use deadline-based and platform-implemented background processing for macOS --- .../src/OSX/platformthreading.mm | 67 ++++++++++++++----- .../Threading/Dispatcher.Queue.cs | 27 +++++--- src/Avalonia.Base/Threading/Dispatcher.cs | 4 ++ .../Threading/IDispatcherImpl.cs | 12 +++- src/Avalonia.Native/DispatcherImpl.cs | 10 ++- src/Avalonia.Native/avn.idl | 3 +- 6 files changed, 93 insertions(+), 30 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index d2d7a365a6..d80df68fea 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -66,25 +66,44 @@ static double distantFutureInterval = (double)50*365*24*3600; ComPtr _events; bool _wakeupDelegateSent; bool _signaled; + bool _backgroundProcessingRequested; CFRunLoopObserverRef _observer; CFRunLoopTimerRef _timer; } +- (void) checkSignaled +{ + bool signaled; + @synchronized (self) { + signaled = _signaled; + _signaled = false; + } + if(signaled) + { + _events->Signaled(); + } +} + - (Signaler*) init { _observer = CFRunLoopObserverCreateWithHandler(nil, - kCFRunLoopBeforeSources | kCFRunLoopAfterWaiting, + kCFRunLoopBeforeSources + | kCFRunLoopAfterWaiting + | kCFRunLoopBeforeWaiting + , true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - bool signaled; - @synchronized (self) { - signaled = self->_signaled; - self->_signaled = false; - } - if(signaled) + if(activity == kCFRunLoopBeforeWaiting) { - self->_events->Signaled(); + bool triggerProcessing; + @synchronized (self) { + triggerProcessing = self->_backgroundProcessingRequested; + self->_backgroundProcessingRequested = false; + } + if(triggerProcessing) + self->_events->ReadyForBackgroundProcessing(); } + [self checkSignaled]; }); CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); @@ -135,10 +154,27 @@ static double distantFutureInterval = (double)50*365*24*3600; if(_signaled) return; _signaled = true; + dispatch_async(dispatch_get_main_queue(), ^{ + [self checkSignaled]; + }); CFRunLoopWakeUp(CFRunLoopGetMain()); } } +- (void) requestBackgroundProcessing +{ + @synchronized (self) { + if(_backgroundProcessingRequested) + return; + _backgroundProcessingRequested = true; + dispatch_async(dispatch_get_main_queue(), ^{ + // This is needed to wakeup the loop if we are called from inside of BeforeWait hook + }); + } + + +} + @end @@ -165,15 +201,7 @@ public: return [NSThread isMainThread]; }; - bool HasPendingInput() override - { - auto event = [NSApp - nextEventMatchingMask: NSEventMaskAny - untilDate:nil - inMode:NSDefaultRunLoopMode - dequeue:false]; - return event != nil; - }; + void SetEvents(IAvnPlatformThreadingInterfaceEvents *cb) override { @@ -227,6 +255,11 @@ public: [_signaler updateTimer:ms]; }; + void RequestBackgroundProcessing() override { + [_signaler requestBackgroundProcessing]; + } + + }; extern IAvnPlatformThreadingInterface* CreatePlatformThreading() diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 0c5414e6d1..105019f277 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -7,13 +7,21 @@ public partial class Dispatcher { private readonly DispatcherPriorityQueue _queue = new(); private bool _signaled; + private bool _explicitBackgroundProcessingRequested; private const int MaximumTimeProcessingBackgroundJobs = 50; void RequestBackgroundProcessing() { lock (InstanceLock) { - if (_dueTimeForBackgroundProcessing == null) + if (_backgroundProcessingImpl != null) + { + if(_explicitBackgroundProcessingRequested) + return; + _explicitBackgroundProcessingRequested = true; + _backgroundProcessingImpl.RequestBackgroundProcessing(); + } + else if (_dueTimeForBackgroundProcessing == null) { _dueTimeForBackgroundProcessing = Clock.TickCount + 1; UpdateOSTimer(); @@ -21,6 +29,15 @@ public partial class Dispatcher } } + private void OnReadyForExplicitBackgroundProcessing() + { + lock (InstanceLock) + { + _explicitBackgroundProcessingRequested = false; + ExecuteJobsCore(); + } + } + /// /// Force-runs all dispatcher operations ignoring any pending OS events, use with caution /// @@ -124,13 +141,7 @@ public partial class Dispatcher else if (_pendingInputImpl?.CanQueryPendingInput == true) { if (!_pendingInputImpl.HasPendingInput) - { - // On platforms like macOS HasPendingInput check might trigger a timer - // which would result in reentrancy here, so we check if the job - // hasn't been executed yet - if (job.Status == DispatcherOperationStatus.Pending) - ExecuteJob(job); - } + ExecuteJob(job); else { RequestBackgroundProcessing(); diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 9a0dacfbb8..d1bd15e286 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -23,6 +23,7 @@ public partial class Dispatcher : IDispatcher private IControlledDispatcherImpl? _controlledImpl; private static Dispatcher? s_uiThread; private IDispatcherImplWithPendingInput? _pendingInputImpl; + private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; internal Dispatcher(IDispatcherImpl impl, IDispatcherClock clock) { @@ -32,6 +33,9 @@ public partial class Dispatcher : IDispatcher impl.Signaled += Signaled; _controlledImpl = _impl as IControlledDispatcherImpl; _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; + _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; + if (_backgroundProcessingImpl != null) + _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; } public static Dispatcher UIThread => s_uiThread ??= CreateUIThreadDispatcher(); diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 7f1e352b51..2cc06d1986 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -1,9 +1,11 @@ using System; using System.Threading; +using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.Threading; +[Unstable] public interface IDispatcherImpl { bool CurrentThreadIsLoopThread { get; } @@ -15,7 +17,7 @@ public interface IDispatcherImpl void UpdateTimer(int? dueTimeInMs); } - +[Unstable] public interface IDispatcherImplWithPendingInput : IDispatcherImpl { // Checks if dispatcher implementation can @@ -24,6 +26,14 @@ public interface IDispatcherImplWithPendingInput : IDispatcherImpl bool HasPendingInput { get; } } +[Unstable] +public interface IDispatcherImplWithExplicitBackgroundProcessing : IDispatcherImpl +{ + event Action ReadyForBackgroundProcessing; + void RequestBackgroundProcessing(); +} + +[Unstable] public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput { // Runs the event loop diff --git a/src/Avalonia.Native/DispatcherImpl.cs b/src/Avalonia.Native/DispatcherImpl.cs index a9e5e6deb1..b1d3cb59de 100644 --- a/src/Avalonia.Native/DispatcherImpl.cs +++ b/src/Avalonia.Native/DispatcherImpl.cs @@ -10,7 +10,7 @@ using MicroCom.Runtime; namespace Avalonia.Native; -internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock +internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock, IDispatcherImplWithExplicitBackgroundProcessing { private readonly IAvnPlatformThreadingInterface _native; private Thread? _loopThread; @@ -25,6 +25,7 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public event Action Signaled; public event Action Timer; + public event Action ReadyForBackgroundProcessing; private class Events : NativeCallbackBase, IAvnPlatformThreadingInterfaceEvents { @@ -37,6 +38,8 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public void Signaled() => _parent.Signaled?.Invoke(); public void Timer() => _parent.Timer?.Invoke(); + + public void ReadyForBackgroundProcessing() => _parent.ReadyForBackgroundProcessing?.Invoke(); } public bool CurrentThreadIsLoopThread @@ -60,8 +63,8 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock _native.UpdateTimer(ms); } - public bool CanQueryPendingInput => true; - public bool HasPendingInput => _native.HasPendingInput() != 0; + public bool CanQueryPendingInput => false; + public bool HasPendingInput => false; class RunLoopFrame : IDisposable { @@ -124,4 +127,5 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock frame.Exception = capture; frame.CancellationTokenSource.Cancel(); } + public void RequestBackgroundProcessing() => _native.RequestBackgroundProcessing(); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 7763d0d2fc..09e9168d8f 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -649,6 +649,7 @@ interface IAvnPlatformThreadingInterfaceEvents : IUnknown { void Signaled(); void Timer(); + void ReadyForBackgroundProcessing(); } [uuid(97330f88-c22b-4a8e-a130-201520091b01)] @@ -661,12 +662,12 @@ interface IAvnLoopCancellation : IUnknown interface IAvnPlatformThreadingInterface : IUnknown { bool GetCurrentThreadIsLoopThread(); - bool HasPendingInput(); void SetEvents(IAvnPlatformThreadingInterfaceEvents* cb); IAvnLoopCancellation* CreateLoopCancellation(); void RunLoop(IAvnLoopCancellation* cancel); void Signal(); void UpdateTimer(int ms); + void RequestBackgroundProcessing(); } [uuid(6c621a6e-e4c1-4ae3-9749-83eeeffa09b6)] From dbbdcb95cd0ceb76f777620fa12473e764890726 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 19 Mar 2023 22:19:55 +0600 Subject: [PATCH 14/17] Refactored dispatcher clock --- .../Threading/Dispatcher.Queue.cs | 11 ++++---- .../Threading/Dispatcher.Timers.cs | 18 +++++++------ src/Avalonia.Base/Threading/Dispatcher.cs | 6 ++--- .../Threading/DispatcherTimer.cs | 6 ++--- .../Threading/IDispatcherClock.cs | 13 ---------- .../Threading/IDispatcherImpl.cs | 16 ++++++++---- .../Platform/ManagedDispatcherImpl.cs | 3 ++- src/Avalonia.Native/DispatcherImpl.cs | 9 ++++--- src/Avalonia.X11/X11PlatformThreading.cs | 6 ++--- .../Avalonia.Win32/Win32DispatcherImpl.cs | 10 ++++--- .../DispatcherTests.cs | 26 +++++++++---------- 11 files changed, 61 insertions(+), 63 deletions(-) delete mode 100644 src/Avalonia.Base/Threading/IDispatcherClock.cs diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 105019f277..c91af1a514 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -23,7 +23,7 @@ public partial class Dispatcher } else if (_dueTimeForBackgroundProcessing == null) { - _dueTimeForBackgroundProcessing = Clock.TickCount + 1; + _dueTimeForBackgroundProcessing = Now + 1; UpdateOSTimer(); } } @@ -68,7 +68,8 @@ public partial class Dispatcher public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInMs) + public long Now => 0; + public void UpdateTimer(long? dueTimeInMs) { } } @@ -119,7 +120,7 @@ public partial class Dispatcher void ExecuteJobsCore() { - int? backgroundJobExecutionStartedAt = null; + long? backgroundJobExecutionStartedAt = null; while (true) { DispatcherOperation? job; @@ -153,9 +154,9 @@ public partial class Dispatcher else { if (backgroundJobExecutionStartedAt == null) - backgroundJobExecutionStartedAt = Clock.TickCount; + backgroundJobExecutionStartedAt = Now; - if (Clock.TickCount - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) + if (Now - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) { _signaled = true; RequestBackgroundProcessing(); diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index 269d10707e..bb252b7f55 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -9,11 +9,13 @@ public partial class Dispatcher private List _timers = new(); private long _timersVersion; private bool _dueTimeFound; - private int _dueTimeInMs; + private long _dueTimeInMs; - private int? _dueTimeForTimers; - private int? _dueTimeForBackgroundProcessing; - private int? _osTimerSetTo; + private long? _dueTimeForTimers; + private long? _dueTimeForBackgroundProcessing; + private long? _osTimerSetTo; + + internal long Now => _impl.Now; private void UpdateOSTimer() { @@ -40,7 +42,7 @@ public partial class Dispatcher if (!_hasShutdownFinished) // Dispatcher thread, does not technically need the lock to read { bool oldDueTimeFound = _dueTimeFound; - int oldDueTimeInTicks = _dueTimeInMs; + long oldDueTimeInTicks = _dueTimeInMs; _dueTimeFound = false; _dueTimeInMs = 0; @@ -113,11 +115,11 @@ public partial class Dispatcher lock (InstanceLock) { _impl.UpdateTimer(_osTimerSetTo = null); - needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Clock.TickCount; + needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Now; if (needToPromoteTimers) _dueTimeForTimers = null; needToProcessQueue = _dueTimeForBackgroundProcessing.HasValue && - _dueTimeForBackgroundProcessing.Value <= Clock.TickCount; + _dueTimeForBackgroundProcessing.Value <= Now; if (needToProcessQueue) _dueTimeForBackgroundProcessing = null; } @@ -131,7 +133,7 @@ public partial class Dispatcher internal void PromoteTimers() { - int currentTimeInTicks = Clock.TickCount; + long currentTimeInTicks = Now; try { List? timers = null; diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index d1bd15e286..25a4a4ce2c 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -17,7 +17,6 @@ namespace Avalonia.Threading; public partial class Dispatcher : IDispatcher { private IDispatcherImpl _impl; - internal IDispatcherClock Clock { get; } internal object InstanceLock { get; } = new(); private bool _hasShutdownFinished; private IControlledDispatcherImpl? _controlledImpl; @@ -25,10 +24,9 @@ public partial class Dispatcher : IDispatcher private IDispatcherImplWithPendingInput? _pendingInputImpl; private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; - internal Dispatcher(IDispatcherImpl impl, IDispatcherClock clock) + internal Dispatcher(IDispatcherImpl impl) { _impl = impl; - Clock = clock; impl.Timer += OnOSTimer; impl.Signaled += Signaled; _controlledImpl = _impl as IControlledDispatcherImpl; @@ -51,7 +49,7 @@ public partial class Dispatcher : IDispatcher else impl = new NullDispatcherImpl(); } - return new Dispatcher(impl, impl as IDispatcherClock ?? new DefaultDispatcherClock()); + return new Dispatcher(impl); } /// diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 0c235ee161..879d9d8a5f 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -125,7 +125,7 @@ public partial class DispatcherTimer if (_isEnabled) { - DueTimeInMs = _dispatcher.Clock.TickCount + (int)_interval.TotalMilliseconds; + DueTimeInMs = _dispatcher.Now + (long)_interval.TotalMilliseconds; updateOSTimer = true; } } @@ -288,7 +288,7 @@ public partial class DispatcherTimer // BeginInvoke a new operation. _operation = _dispatcher.InvokeAsync(FireTick, DispatcherPriority.Inactive); - DueTimeInMs = _dispatcher.Clock.TickCount + (int)_interval.TotalMilliseconds; + DueTimeInMs = _dispatcher.Now + (long)_interval.TotalMilliseconds; if (_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess()) { @@ -348,5 +348,5 @@ public partial class DispatcherTimer private bool _isEnabled; // used by Dispatcher - internal int DueTimeInMs { get; private set; } + internal long DueTimeInMs { get; private set; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/IDispatcherClock.cs b/src/Avalonia.Base/Threading/IDispatcherClock.cs deleted file mode 100644 index 2a5268d192..0000000000 --- a/src/Avalonia.Base/Threading/IDispatcherClock.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Avalonia.Threading; - -internal interface IDispatcherClock -{ - int TickCount { get; } -} - -internal class DefaultDispatcherClock : IDispatcherClock -{ - public int TickCount => Environment.TickCount; -} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 2cc06d1986..670ec55461 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading; using Avalonia.Metadata; using Avalonia.Platform; @@ -14,7 +15,8 @@ public interface IDispatcherImpl void Signal(); event Action Signaled; event Action Timer; - void UpdateTimer(int? dueTimeInMs); + long Now { get; } + void UpdateTimer(long? dueTimeInMs); } [Unstable] @@ -40,10 +42,11 @@ public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput void RunLoop(CancellationToken token); } -internal class LegacyDispatcherImpl : DefaultDispatcherClock, IControlledDispatcherImpl +internal class LegacyDispatcherImpl : IControlledDispatcherImpl { private readonly IPlatformThreadingInterface _platformThreading; private IDisposable? _timer; + private Stopwatch _clock = Stopwatch.StartNew(); public LegacyDispatcherImpl(IPlatformThreadingInterface platformThreading) { @@ -56,14 +59,15 @@ internal class LegacyDispatcherImpl : DefaultDispatcherClock, IControlledDispatc public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInMs) + public long Now => _clock.ElapsedMilliseconds; + public void UpdateTimer(long? dueTimeInMs) { _timer?.Dispose(); _timer = null; if (dueTimeInMs.HasValue) { - var interval = Math.Max(1, dueTimeInMs.Value - TickCount); + var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds); _timer = _platformThreading.StartTimer(DispatcherPriority.Send, TimeSpan.FromMilliseconds(interval), OnTick); @@ -94,7 +98,9 @@ class NullDispatcherImpl : IDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInMs) + public long Now => 0; + + public void UpdateTimer(long? dueTimeInMs) { } diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index 54c96113ea..20aa91c83e 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -40,7 +40,8 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInMs) + public long Now => _clock.ElapsedMilliseconds; + public void UpdateTimer(long? dueTimeInMs) { lock (_lock) { diff --git a/src/Avalonia.Native/DispatcherImpl.cs b/src/Avalonia.Native/DispatcherImpl.cs index b1d3cb59de..fd8ef567f4 100644 --- a/src/Avalonia.Native/DispatcherImpl.cs +++ b/src/Avalonia.Native/DispatcherImpl.cs @@ -10,10 +10,11 @@ using MicroCom.Runtime; namespace Avalonia.Native; -internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock, IDispatcherImplWithExplicitBackgroundProcessing +internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherImplWithExplicitBackgroundProcessing { private readonly IAvnPlatformThreadingInterface _native; private Thread? _loopThread; + private Stopwatch _clock = Stopwatch.StartNew(); private Stack _managedFrames = new(); public DispatcherImpl(IAvnPlatformThreadingInterface native) @@ -57,9 +58,9 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock, IDi public void Signal() => _native.Signal(); - public void UpdateTimer(int? dueTimeInMs) + public void UpdateTimer(long? dueTimeInMs) { - var ms = dueTimeInMs == null ? -1 : Math.Max(1, dueTimeInMs.Value - TickCount); + var ms = dueTimeInMs == null ? -1 : (int)Math.Min(int.MaxValue - 10, Math.Max(1, dueTimeInMs.Value - Now)); _native.UpdateTimer(ms); } @@ -113,7 +114,7 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock, IDi } } - public int TickCount => Environment.TickCount; + public long Now => _clock.ElapsedMilliseconds; public void PropagateCallbackException(ExceptionDispatchInfo capture) { diff --git a/src/Avalonia.X11/X11PlatformThreading.cs b/src/Avalonia.X11/X11PlatformThreading.cs index f2f45bce8e..de0e3bee5d 100644 --- a/src/Avalonia.X11/X11PlatformThreading.cs +++ b/src/Avalonia.X11/X11PlatformThreading.cs @@ -9,7 +9,7 @@ using static Avalonia.X11.XLib; namespace Avalonia.X11 { - internal unsafe class X11PlatformThreading : IControlledDispatcherImpl, IDispatcherClock + internal unsafe class X11PlatformThreading : IControlledDispatcherImpl { private readonly AvaloniaX11Platform _platform; private readonly IntPtr _display; @@ -227,7 +227,7 @@ namespace Avalonia.X11 public event Action Signaled; public event Action Timer; - public void UpdateTimer(int? dueTimeInMs) + public void UpdateTimer(long? dueTimeInMs) { _nextTimer = dueTimeInMs; if (_nextTimer != null) @@ -235,7 +235,7 @@ namespace Avalonia.X11 } - public int TickCount => (int)_clock.ElapsedMilliseconds; + public long Now => (int)_clock.ElapsedMilliseconds; public bool CanQueryPendingInput => true; public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || XPending(_display) != 0; diff --git a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs index 3c2f7842ba..581e5fa306 100644 --- a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs +++ b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Threading; @@ -6,10 +7,11 @@ using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32; -internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock +internal class Win32DispatcherImpl : IControlledDispatcherImpl { private readonly IntPtr _messageWindow; private static Thread? s_uiThread; + private readonly Stopwatch _clock = Stopwatch.StartNew(); public Win32DispatcherImpl(IntPtr messageWindow) { _messageWindow = messageWindow; @@ -36,7 +38,7 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public void FireTimer() => Timer?.Invoke(); - public void UpdateTimer(int? dueTimeInMs) + public void UpdateTimer(long? dueTimeInMs) { if (dueTimeInMs == null) { @@ -44,7 +46,7 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock } else { - var interval = (uint)Math.Max(1, TickCount - dueTimeInMs.Value); + var interval = (uint)Math.Min(int.MaxValue - 10, Math.Max(1, Now - dueTimeInMs.Value)); SetTimer( _messageWindow, (IntPtr)Win32Platform.TIMERID_DISPATCHER, @@ -115,5 +117,5 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock } } - public int TickCount => Environment.TickCount; + public long Now => _clock.ElapsedMilliseconds; } diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index 902af94121..38175ad410 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -7,7 +7,7 @@ namespace Avalonia.Base.UnitTests; public class DispatcherTests { - class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherClock, IDispatcherImplWithPendingInput + class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherImplWithPendingInput { public bool CurrentThreadIsLoopThread => true; @@ -15,15 +15,15 @@ public class DispatcherTests public event Action Signaled; public event Action Timer; - public int? NextTimer { get; private set; } + public long? NextTimer { get; private set; } public bool AskedForSignal { get; private set; } - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(long? dueTimeInTicks) { NextTimer = dueTimeInTicks; } - public int TickCount { get; set; } + public long Now { get; set; } public void ExecuteSignal() { @@ -37,7 +37,7 @@ public class DispatcherTests { if (NextTimer == null) return; - TickCount = NextTimer.Value; + Now = NextTimer.Value; Timer?.Invoke(); } @@ -51,7 +51,7 @@ public class DispatcherTests public void DispatcherExecutesJobsAccordingToPriority() { var impl = new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl, impl); + var disp = new Dispatcher(impl); var actions = new List(); disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background); disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render); @@ -65,7 +65,7 @@ public class DispatcherTests public void DispatcherPreservesOrderWhenChangingPriority() { var impl = new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl, impl); + var disp = new Dispatcher(impl); var actions = new List(); var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background); var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input); @@ -84,7 +84,7 @@ public class DispatcherTests public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached() { var impl = new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl, impl); + var disp = new Dispatcher(impl); var actions = new List(); for (var c = 0; c < 10; c++) { @@ -92,7 +92,7 @@ public class DispatcherTests disp.Post(() => { actions.Add(itemId); - impl.TickCount += 20; + impl.Now += 20; }, DispatcherPriority.Background); } @@ -114,7 +114,7 @@ public class DispatcherTests Assert.False(impl.AskedForSignal); if (c < 3) { - Assert.True(impl.NextTimer > impl.TickCount); + Assert.True(impl.NextTimer > impl.Now); } else Assert.Null(impl.NextTimer); @@ -127,7 +127,7 @@ public class DispatcherTests { var impl = new SimpleDispatcherImpl(); impl.TestInputPending = true; - var disp = new Dispatcher(impl, impl); + var disp = new Dispatcher(impl); var actions = new List(); for (var c = 0; c < 10; c++) { @@ -160,8 +160,8 @@ public class DispatcherTests Assert.False(impl.AskedForSignal); if (c < 3) { - Assert.True(impl.NextTimer > impl.TickCount); - impl.TickCount = impl.NextTimer.Value + 1; + Assert.True(impl.NextTimer > impl.Now); + impl.Now = impl.NextTimer.Value + 1; } else Assert.Null(impl.NextTimer); From 17492be5b6fed1c94c13a6477869d98f0c20e8d0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 20 Mar 2023 13:26:16 +0600 Subject: [PATCH 15/17] Removed RunLoop from IPlatformThreadingInterface --- src/Android/Avalonia.Android/AndroidThreadingInterface.cs | 2 -- src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs | 2 -- .../Rendering/Composition/Transport/BatchStreamArrayPool.cs | 3 ++- src/Avalonia.Base/Threading/IDispatcherImpl.cs | 6 +----- src/Browser/Avalonia.Browser/WindowingPlatform.cs | 5 ----- src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs | 5 ----- .../AvaloniaObjectTests_Threading.cs | 5 ----- tests/Avalonia.Benchmarks/NullThreadingPlatform.cs | 4 ---- tests/Avalonia.RenderTests/TestBase.cs | 5 ----- 9 files changed, 3 insertions(+), 34 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs index 152076013f..c85d5b1343 100644 --- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs +++ b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs @@ -21,8 +21,6 @@ namespace Avalonia.Android _handler = new Handler(App.Context.MainLooper); } - public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException(); - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { if (interval.TotalMilliseconds < 10) diff --git a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs index 3dbc7c1bb2..4fe4a2e6b9 100644 --- a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs @@ -11,8 +11,6 @@ namespace Avalonia.Platform [Unstable] public interface IPlatformThreadingInterface { - void RunLoop(CancellationToken cancellationToken); - /// /// Starts a timer. /// diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index f24f449551..032a3046d7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -30,7 +30,8 @@ internal abstract class BatchStreamPoolBase : IDisposable GC.SuppressFinalize(needsFinalize); var updateRef = new WeakReference>(this); - if (AvaloniaLocator.Current.GetService() == null) + if (AvaloniaLocator.Current.GetService() == null + && AvaloniaLocator.Current.GetService() == null) _reclaimImmediately = true; else StartUpdateTimer(startTimer, updateRef); diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 670ec55461..4c30e2eb2c 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -42,7 +42,7 @@ public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput void RunLoop(CancellationToken token); } -internal class LegacyDispatcherImpl : IControlledDispatcherImpl +internal class LegacyDispatcherImpl : IDispatcherImpl { private readonly IPlatformThreadingInterface _platformThreading; private IDisposable? _timer; @@ -80,10 +80,6 @@ internal class LegacyDispatcherImpl : IControlledDispatcherImpl _timer = null; Timer?.Invoke(); } - - public bool CanQueryPendingInput => false; - public bool HasPendingInput => false; - public void RunLoop(CancellationToken token) => _platformThreading.RunLoop(token); } class NullDispatcherImpl : IDispatcherImpl diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index 46db8936eb..0578a08ec3 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -49,11 +49,6 @@ namespace Avalonia.Browser .Bind().ToSingleton(); } - public void RunLoop(CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { return GetRuntimePlatform() diff --git a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs b/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs index fa36ab6c79..93caecf711 100644 --- a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs +++ b/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs @@ -14,11 +14,6 @@ namespace Avalonia.iOS public bool CurrentThreadIsLoopThread => NSThread.Current.IsMainThread; public event Action Signaled; - public void RunLoop(CancellationToken cancellationToken) - { - //Mobile platforms are using external main loop - throw new NotSupportedException(); - } public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) => NSTimer.CreateRepeatingScheduledTimer(interval, _ => tick()); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs index 7b68852f03..16d6b9166b 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs @@ -161,11 +161,6 @@ namespace Avalonia.Base.UnitTests public event Action Signaled; #pragma warning restore 67 - public void RunLoop(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public void Signal(DispatcherPriority prio) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs b/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs index bb469a6b33..6acc9e3483 100644 --- a/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs @@ -8,10 +8,6 @@ namespace Avalonia.Benchmarks { internal class NullThreadingPlatform : IPlatformThreadingInterface { - public void RunLoop(CancellationToken cancellationToken) - { - } - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { return Disposable.Empty; diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index fd258dfd96..b6aba5e04c 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -253,11 +253,6 @@ namespace Avalonia.Direct2D1.RenderTests public event Action Signaled; #pragma warning restore 67 - public void RunLoop(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public void Signal(DispatcherPriority prio) { // No-op From 0f4ed2a4b9fb82f9eff66fce66ef6ae9758fe594 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 20 Mar 2023 15:15:53 +0600 Subject: [PATCH 16/17] Updated tests to use IDispatcherImpl --- .../AvaloniaObjectTests_Binding.cs | 20 ++++----- .../AvaloniaObjectTests_Direct.cs | 6 +-- .../AvaloniaObjectTests_Threading.cs | 43 +++++++++---------- .../Input/MouseDeviceTests.cs | 7 +-- .../Input/TouchDeviceTests.cs | 3 +- .../Layout/ControlsBenchmark.cs | 2 +- .../NullThreadingPlatform.cs | 14 +++--- .../Styling/ControlTheme_Change.cs | 2 +- .../Styling/ResourceBenchmarks.cs | 2 +- .../Styling/Style_Apply_Detach_Complex.cs | 2 +- .../Text/HugeTextLayout.cs | 2 +- .../Themes/FluentBenchmark.cs | 2 +- .../DesktopStyleApplicationLifetimeTests.cs | 8 +++- .../Primitives/SelectingItemsControlTests.cs | 10 ++--- tests/Avalonia.RenderTests/TestBase.cs | 33 +++++--------- tests/Avalonia.UnitTests/TestServices.cs | 15 ++++--- .../Avalonia.UnitTests/UnitTestApplication.cs | 2 +- 17 files changed, 81 insertions(+), 92 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 9f74d2fc08..e4080dd0a9 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -962,12 +962,12 @@ namespace Avalonia.Base.UnitTests var currentThreadId = Thread.CurrentThread.ManagedThreadId; var raised = 0; - var threadingInterfaceMock = new Mock(); - threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + var dispatcherMock = new Mock(); + dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); var services = new TestServices( - threadingInterface: threadingInterfaceMock.Object); + dispatcherImpl: dispatcherMock.Object); target.PropertyChanged += (s, e) => { @@ -1000,12 +1000,12 @@ namespace Avalonia.Base.UnitTests var currentThreadId = Thread.CurrentThread.ManagedThreadId; var raised = 0; - var threadingInterfaceMock = new Mock(); - threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + var dispatcherMock = new Mock(); + dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); var services = new TestServices( - threadingInterface: threadingInterfaceMock.Object); + dispatcherImpl: dispatcherMock.Object); target.PropertyChanged += (s, e) => { @@ -1038,12 +1038,12 @@ namespace Avalonia.Base.UnitTests var currentThreadId = Thread.CurrentThread.ManagedThreadId; var raised = 0; - var threadingInterfaceMock = new Mock(); + var threadingInterfaceMock = new Mock(); threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); var services = new TestServices( - threadingInterface: threadingInterfaceMock.Object); + dispatcherImpl: threadingInterfaceMock.Object); target.PropertyChanged += (s, e) => { @@ -1071,12 +1071,12 @@ namespace Avalonia.Base.UnitTests var source = new Subject(); var currentThreadId = Thread.CurrentThread.ManagedThreadId; - var threadingInterfaceMock = new Mock(); + var threadingInterfaceMock = new Mock(); threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); var services = new TestServices( - threadingInterface: threadingInterfaceMock.Object); + dispatcherImpl: threadingInterfaceMock.Object); using (UnitTestApplication.Start(services)) { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 973090ee92..ad8c2751cd 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -530,12 +530,12 @@ namespace Avalonia.Base.UnitTests var currentThreadId = Thread.CurrentThread.ManagedThreadId; var raised = 0; - var threadingInterfaceMock = new Mock(); - threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + var dispatcherMock = new Mock(); + dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); var services = new TestServices( - threadingInterface: threadingInterfaceMock.Object); + dispatcherImpl: dispatcherMock.Object); target.PropertyChanged += (s, e) => { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs index 16d6b9166b..7f1251f73e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs @@ -11,12 +11,12 @@ namespace Avalonia.Base.UnitTests { public class AvaloniaObjectTests_Threading { - private ThreadingInterface _threading = new ThreadingInterface(true); + private TestDipatcherImpl _threading = new(true); [Fact] public void AvaloniaObject_Constructor_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface()))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: new TestDipatcherImpl()))) { Assert.Throws(() => new Class1()); } @@ -25,7 +25,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void StyledProperty_GetValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -36,7 +36,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void StyledProperty_SetValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -47,7 +47,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void Setting_StyledProperty_Binding_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -61,7 +61,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void StyledProperty_ClearValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -72,7 +72,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void StyledProperty_IsSet_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void DirectProperty_GetValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -94,7 +94,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void DirectProperty_SetValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -105,7 +105,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void Setting_DirectProperty_Binding_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -119,7 +119,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void DirectProperty_ClearValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -130,7 +130,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void DirectProperty_IsSet_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading))) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) { var target = new Class1(); _threading.CurrentThreadIsLoopThread = false; @@ -147,10 +147,10 @@ namespace Avalonia.Base.UnitTests AvaloniaProperty.RegisterDirect("Qux", _ => null, (o, v) => { }); } - private class ThreadingInterface : IPlatformThreadingInterface + private class TestDipatcherImpl : IDispatcherImpl { - public ThreadingInterface(bool isLoopThread = false) + public TestDipatcherImpl(bool isLoopThread = false) { CurrentThreadIsLoopThread = isLoopThread; } @@ -158,18 +158,17 @@ namespace Avalonia.Base.UnitTests public bool CurrentThreadIsLoopThread { get; set; } #pragma warning disable 67 - public event Action Signaled; -#pragma warning restore 67 - - public void Signal(DispatcherPriority prio) + public event Action Signaled; + public event Action Timer; + public long Now => 0; + public void UpdateTimer(long? dueTimeInMs) { throw new NotImplementedException(); } + public void Signal() => throw new NotImplementedException(); +#pragma warning restore 67 - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) - { - throw new NotImplementedException(); - } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs index 1bb1b4af73..642e762c1b 100644 --- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs @@ -3,6 +3,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.UnitTests; using Moq; using Xunit; @@ -16,9 +17,9 @@ namespace Avalonia.Base.UnitTests.Input { using var scope = AvaloniaLocator.EnterScope(); var settingsMock = new Mock(); - var threadingMock = new Mock(); + var dispatcherMock = new Mock(); - threadingMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true); + dispatcherMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true); AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(settingsMock.Object); @@ -26,7 +27,7 @@ namespace Avalonia.Base.UnitTests.Input using var app = UnitTestApplication.Start( new TestServices( inputManager: new InputManager(), - threadingInterface: threadingMock.Object)); + dispatcherImpl: dispatcherMock.Object)); var renderer = RendererMocks.CreateRenderer(); var device = new MouseDevice(); diff --git a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs index 36587ea222..0653d49191 100644 --- a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input.Raw; using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.UnitTests; using Moq; using Xunit; @@ -207,7 +208,7 @@ namespace Avalonia.Input.UnitTests private IDisposable UnitTestApp(TimeSpan doubleClickTime = new TimeSpan()) { var unitTestApp = UnitTestApplication.Start( - new TestServices(inputManager: new InputManager(), threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true))); + new TestServices(inputManager: new InputManager(), dispatcherImpl: Mock.Of(x => x.CurrentThreadIsLoopThread == true))); var iSettingsMock = new Mock(); iSettingsMock.Setup(x => x.GetDoubleTapTime(It.IsAny())).Returns(doubleClickTime); iSettingsMock.Setup(x => x.GetDoubleTapSize(It.IsAny())).Returns(new Size(16, 16)); diff --git a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs index eacf73ac94..5df1dd1679 100644 --- a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs @@ -18,7 +18,7 @@ namespace Avalonia.Benchmarks.Layout _app = UnitTestApplication.Start( TestServices.StyledWindow.With( renderInterface: new NullRenderingPlatform(), - threadingInterface: new NullThreadingPlatform(), + dispatcherImpl: new NullThreadingPlatform(), standardCursorFactory: new NullCursorFactory())); _root = new TestRoot(true, null) diff --git a/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs b/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs index 6acc9e3483..1b5b60031c 100644 --- a/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs @@ -6,22 +6,22 @@ using Avalonia.Threading; namespace Avalonia.Benchmarks { - internal class NullThreadingPlatform : IPlatformThreadingInterface + internal class NullThreadingPlatform : IDispatcherImpl { - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) + public void Signal() { - return Disposable.Empty; } - - public void Signal(DispatcherPriority priority) + + public void UpdateTimer(long? dueTimeInMs) { } public bool CurrentThreadIsLoopThread => true; #pragma warning disable CS0067 - public event Action Signaled; + public event Action Signaled; + public event Action Timer; + public long Now => 0; #pragma warning restore CS0067 - } } diff --git a/tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs b/tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs index 627edfdeb6..00080e3b82 100644 --- a/tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs +++ b/tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs @@ -23,7 +23,7 @@ namespace Avalonia.Benchmarks.Styling _app = UnitTestApplication.Start( TestServices.StyledWindow.With( renderInterface: new NullRenderingPlatform(), - threadingInterface: new NullThreadingPlatform())); + dispatcherImpl: new NullThreadingPlatform())); // Simulate an application with a lot of styles by creating a tree of nested panels, // each with a bunch of styles applied. diff --git a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs index bc47e68bc1..b044bcde59 100644 --- a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs +++ b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs @@ -23,7 +23,7 @@ namespace Avalonia.Benchmarks.Styling renderInterface: new MockPlatformRenderInterface(), standardCursorFactory: Mock.Of(), theme: () => CreateTheme(), - threadingInterface: new NullThreadingPlatform(), + dispatcherImpl: new NullThreadingPlatform(), fontManagerImpl: new MockFontManagerImpl(), textShaperImpl: new MockTextShaperImpl(), windowingPlatform: new MockWindowingPlatform()); diff --git a/tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs b/tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs index a6be6c0c18..307f15a6c0 100644 --- a/tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs +++ b/tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs @@ -20,7 +20,7 @@ namespace Avalonia.Benchmarks.Styling _app = UnitTestApplication.Start( TestServices.StyledWindow.With( renderInterface: new NullRenderingPlatform(), - threadingInterface: new NullThreadingPlatform())); + dispatcherImpl: new NullThreadingPlatform())); // Simulate an application with a lot of styles by creating a tree of nested panels, // each with a bunch of styles applied. diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 03b85840a7..22ca9d8c6d 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -32,7 +32,7 @@ public class HugeTextLayout : IDisposable var testServices = TestServices.StyledWindow.With( renderInterface: new NullRenderingPlatform(), - threadingInterface: new NullThreadingPlatform(), + dispatcherImpl: new NullThreadingPlatform(), standardCursorFactory: new NullCursorFactory()); if (s_useSkia) diff --git a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs index 1115bc9760..e9b82d5381 100644 --- a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs @@ -47,7 +47,7 @@ namespace Avalonia.Benchmarks.Themes renderInterface: new MockPlatformRenderInterface(), standardCursorFactory: Mock.Of(), theme: () => LoadFluentTheme(), - threadingInterface: new NullThreadingPlatform(), + dispatcherImpl: new NullThreadingPlatform(), fontManagerImpl: new MockFontManagerImpl(), textShaperImpl: new MockTextShaperImpl(), windowingPlatform: new MockWindowingPlatform()); diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 8cd5816984..90a0db691c 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Threading; using Avalonia.UnitTests; using Moq; using Xunit; @@ -12,10 +13,13 @@ namespace Avalonia.Controls.UnitTests public class DesktopStyleApplicationLifetimeTests { + IDispatcherImpl CreateDispatcherWithInstantMainLoop() => Mock.Of(x => x.CurrentThreadIsLoopThread == true); + + [Fact] public void Should_Set_ExitCode_After_Shutdown() { - using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + using (UnitTestApplication.Start(new TestServices(dispatcherImpl: CreateDispatcherWithInstantMainLoop()))) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.Shutdown(1337); @@ -215,7 +219,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event() { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: CreateDispatcherWithInstantMainLoop()))) using (var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var lifetimeEvents = new Mock(); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 3fa2bb515d..9574598038 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -2084,13 +2084,9 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Setting_IsTextSearchEnabled_Enables_Or_Disables_Text_Search() { - var pti = Mock.Of(x => x.CurrentThreadIsLoopThread == true); - - Mock.Get(pti) - .Setup(v => v.StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Disposable.Empty); - - using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti))) + var pti = Mock.Of(x => x.CurrentThreadIsLoopThread == true); + + using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: pti))) { var items = new[] { diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index b6aba5e04c..a0d8f8bcc3 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -41,8 +41,8 @@ namespace Avalonia.Direct2D1.RenderTests #endif public static FontFamily TestFontFamily = new FontFamily(s_fontUri); - private static readonly TestThreadingInterface threadingInterface = - new TestThreadingInterface(); + private static readonly TestDispatcherImpl threadingInterface = + new TestDispatcherImpl(); private static readonly IAssetLoader assetLoader = new AssetLoader(); @@ -54,7 +54,7 @@ namespace Avalonia.Direct2D1.RenderTests Direct2D1Platform.Initialize(); #endif AvaloniaLocator.CurrentMutable - .Bind() + .Bind() .ToConstant(threadingInterface); AvaloniaLocator.CurrentMutable @@ -243,40 +243,27 @@ namespace Avalonia.Direct2D1.RenderTests return path; } - private class TestThreadingInterface : IPlatformThreadingInterface + private class TestDispatcherImpl : IDispatcherImpl { public bool CurrentThreadIsLoopThread => MainThread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId; public Thread MainThread { get; set; } #pragma warning disable 67 - public event Action Signaled; + public event Action Signaled; + public event Action Timer; #pragma warning restore 67 - public void Signal(DispatcherPriority prio) + public void Signal() { // No-op } - private static List s_timers = new(); - - public static void RunTimers() - { - lock (s_timers) - { - foreach(var t in s_timers.ToList()) - t.Invoke(); - } - } + public long Now => 0; - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) + public void UpdateTimer(long? dueTimeInMs) { - var act = () => tick(); - lock (s_timers) s_timers.Add(act); - return Disposable.Create(() => - { - lock (s_timers) s_timers.Remove(act); - }); + // No-op } } diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 339cb1462c..800abbc2c7 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using Avalonia.Controls; using System.Reflection; using Avalonia.Animation; +using Avalonia.Threading; namespace Avalonia.UnitTests { @@ -24,7 +25,7 @@ namespace Avalonia.UnitTests renderInterface: new MockPlatformRenderInterface(), standardCursorFactory: Mock.Of(), theme: () => CreateSimpleTheme(), - threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true), + dispatcherImpl: Mock.Of(x => x.CurrentThreadIsLoopThread == true), fontManagerImpl: new MockFontManagerImpl(), textShaperImpl: new MockTextShaperImpl(), windowingPlatform: new MockWindowingPlatform()); @@ -39,7 +40,7 @@ namespace Avalonia.UnitTests platform: Mock.Of()); public static readonly TestServices MockThreadingInterface = new TestServices( - threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true)); + dispatcherImpl: Mock.Of(x => x.CurrentThreadIsLoopThread == true)); public static readonly TestServices MockWindowingPlatform = new TestServices( windowingPlatform: new MockWindowingPlatform()); @@ -73,7 +74,7 @@ namespace Avalonia.UnitTests IRenderTimer renderLoop = null, ICursorFactory standardCursorFactory = null, Func theme = null, - IPlatformThreadingInterface threadingInterface = null, + IDispatcherImpl dispatcherImpl = null, IFontManagerImpl fontManagerImpl = null, ITextShaperImpl textShaperImpl = null, IWindowImpl windowImpl = null, @@ -92,7 +93,7 @@ namespace Avalonia.UnitTests TextShaperImpl = textShaperImpl; StandardCursorFactory = standardCursorFactory; Theme = theme; - ThreadingInterface = threadingInterface; + DispatcherImpl = dispatcherImpl; WindowImpl = windowImpl; WindowingPlatform = windowingPlatform; } @@ -110,7 +111,7 @@ namespace Avalonia.UnitTests public ITextShaperImpl TextShaperImpl { get; } public ICursorFactory StandardCursorFactory { get; } public Func Theme { get; } - public IPlatformThreadingInterface ThreadingInterface { get; } + public IDispatcherImpl DispatcherImpl { get; } public IWindowImpl WindowImpl { get; } public IWindowingPlatform WindowingPlatform { get; } @@ -128,7 +129,7 @@ namespace Avalonia.UnitTests IScheduler scheduler = null, ICursorFactory standardCursorFactory = null, Func theme = null, - IPlatformThreadingInterface threadingInterface = null, + IDispatcherImpl dispatcherImpl = null, IFontManagerImpl fontManagerImpl = null, ITextShaperImpl textShaperImpl = null, IWindowImpl windowImpl = null, @@ -148,7 +149,7 @@ namespace Avalonia.UnitTests textShaperImpl: textShaperImpl ?? TextShaperImpl, standardCursorFactory: standardCursorFactory ?? StandardCursorFactory, theme: theme ?? Theme, - threadingInterface: threadingInterface ?? ThreadingInterface, + dispatcherImpl: dispatcherImpl ?? DispatcherImpl, windowingPlatform: windowingPlatform ?? WindowingPlatform, windowImpl: windowImpl ?? WindowImpl); } diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 3108e09315..8d9f08caa1 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -71,7 +71,7 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.RenderInterface) .Bind().ToConstant(Services.FontManagerImpl) .Bind().ToConstant(Services.TextShaperImpl) - .Bind().ToConstant(Services.ThreadingInterface) + .Bind().ToConstant(Services.DispatcherImpl) .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToSingleton(); From 371251d58c8ab0cf65bab6b546e080025e864e7a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Mar 2023 13:06:57 +0600 Subject: [PATCH 17/17] Review comments --- samples/ControlCatalog/App.xaml | 4 ++-- tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs | 1 + .../Avalonia.Direct2D1.RenderTests.csproj | 1 + tests/Avalonia.RenderTests/TestBase.cs | 1 + .../Avalonia.Skia.RenderTests.csproj | 1 + .../Avalonia.UnitTests}/DispatcherTimerHelper.cs | 7 +------ 6 files changed, 7 insertions(+), 8 deletions(-) rename {src/Avalonia.Base/Utilities => tests/Avalonia.UnitTests}/DispatcherTimerHelper.cs (76%) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 2c2e1c4075..3b847adcbb 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -60,7 +60,7 @@ - + diff --git a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs index c53891fa1c..c3630c36b7 100644 --- a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Base.UnitTests.Utilities; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.GestureRecognizers; diff --git a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj index a99421f107..17e1ab0e50 100644 --- a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj +++ b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj @@ -16,6 +16,7 @@ + diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index a0d8f8bcc3..4732099d60 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -17,6 +17,7 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.Threading; +using Avalonia.UnitTests; using Avalonia.Utilities; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; diff --git a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj index 0d182678ef..ca9f5ed974 100644 --- a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj +++ b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs b/tests/Avalonia.UnitTests/DispatcherTimerHelper.cs similarity index 76% rename from src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs rename to tests/Avalonia.UnitTests/DispatcherTimerHelper.cs index a457388fb2..ef7271a2d3 100644 --- a/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs +++ b/tests/Avalonia.UnitTests/DispatcherTimerHelper.cs @@ -1,11 +1,6 @@ using Avalonia.Threading; -namespace Avalonia.Utilities; - -public class DispatcherTimerHelper -{ - -} +namespace Avalonia.UnitTests; public static class DispatcherTimerUtils {