Browse Source

Suppress ExecutionContext flow for render scheduling (#21273)

* Add regression tests

* fix executioncontext leakage

* change DispatcherOperation ctor parameter to accept bool captureExecutionContext
bump-diagnosticssupport
Shirasagi 5 months ago
committed by GitHub
parent
commit
171d932b43
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      src/Avalonia.Base/Media/MediaContext.cs
  2. 10
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  3. 84
      tests/Avalonia.Base.UnitTests/DispatcherTests.cs

6
src/Avalonia.Base/Media/MediaContext.cs

@ -91,7 +91,9 @@ internal partial class MediaContext : ICompositorScheduler
if (_inputMarkerOp == null) if (_inputMarkerOp == null)
{ {
_inputMarkerOp = _dispatcher.InvokeAsync(_inputMarkerHandler, DispatcherPriority.Input); _inputMarkerOp = new DispatcherOperation(_dispatcher, DispatcherPriority.Input, _inputMarkerHandler,
throwOnUiThread: true, captureExecutionContext: false);
_dispatcher.InvokeAsyncImpl(_inputMarkerOp, CancellationToken.None);
_inputMarkerAddedAt = _time.Elapsed; _inputMarkerAddedAt = _time.Elapsed;
} }
else if (!now && (_time.Elapsed - _inputMarkerAddedAt).TotalSeconds > MaxSecondsWithoutInput) else if (!now && (_time.Elapsed - _inputMarkerAddedAt).TotalSeconds > MaxSecondsWithoutInput)
@ -99,7 +101,7 @@ internal partial class MediaContext : ICompositorScheduler
priority = DispatcherPriority.Input; priority = DispatcherPriority.Input;
} }
var renderOp = new DispatcherOperation(_dispatcher, priority, _render, throwOnUiThread: true); var renderOp = new DispatcherOperation(_dispatcher, priority, _render, throwOnUiThread: true, captureExecutionContext: false);
_nextRenderOp = renderOp; _nextRenderOp = renderOp;
_dispatcher.InvokeAsyncImpl(renderOp, CancellationToken.None); _dispatcher.InvokeAsyncImpl(renderOp, CancellationToken.None);
} }

10
src/Avalonia.Base/Threading/DispatcherOperation.cs

@ -44,18 +44,20 @@ public class DispatcherOperation
private DispatcherPriority _priority; private DispatcherPriority _priority;
private readonly ExecutionContext? _executionContext; private readonly ExecutionContext? _executionContext;
internal DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Action callback, bool throwOnUiThread) : internal DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Action callback, bool throwOnUiThread,
this(dispatcher, priority, throwOnUiThread) bool captureExecutionContext = true) :
this(dispatcher, priority, throwOnUiThread, captureExecutionContext)
{ {
Callback = callback; Callback = callback;
} }
private protected DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, bool throwOnUiThread) private protected DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, bool throwOnUiThread,
bool captureExecutionContext = true)
{ {
ThrowOnUiThread = throwOnUiThread; ThrowOnUiThread = throwOnUiThread;
Priority = priority; Priority = priority;
Dispatcher = dispatcher; Dispatcher = dispatcher;
_executionContext = ExecutionContext.Capture(); _executionContext = captureExecutionContext ? ExecutionContext.Capture() : null;
} }
internal string DebugDisplay internal string DebugDisplay

84
tests/Avalonia.Base.UnitTests/DispatcherTests.cs

@ -6,6 +6,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
@ -600,6 +601,19 @@ public partial class DispatcherTests
public AsyncLocal<string?> AsyncLocalField { get; set; } = new AsyncLocal<string?>(); public AsyncLocal<string?> AsyncLocalField { get; set; } = new AsyncLocal<string?>();
} }
private sealed class AsyncLocalMeasureControl(Func<string?> getValue, Action<string?> setValue) : Control
{
public bool RecordMeasure { get; set; }
protected override Size MeasureOverride(Size availableSize)
{
if (RecordMeasure)
setValue(getValue());
return new Size(1, 1);
}
}
[Fact] [Fact]
public async Task ExecutionContextIsPreservedInDispatcherInvokeAsync() public async Task ExecutionContextIsPreservedInDispatcherInvokeAsync()
{ {
@ -697,6 +711,76 @@ public partial class DispatcherTests
Assert.Null(test); Assert.Null(test);
} }
[Fact]
public void MediaContextRenderSchedulingDoesNotCaptureAmbientExecutionContext()
{
var impl = new SimpleDispatcherWithBackgroundProcessingImpl();
using var services = new DispatcherServices(impl);
Dispatcher.InitializeUIThreadDispatcher(impl);
var testObject = new AsyncLocalTestClass();
string? test = "Not measured";
var control = new AsyncLocalMeasureControl(() => testObject.AsyncLocalField.Value, value => test = value);
var root = new TestRoot { Child = control };
root.ExecuteInitialLayoutPass();
control.RecordMeasure = true;
Dispatcher.UIThread.Post(() =>
{
testObject.AsyncLocalField.Value = "Initial Value";
control.InvalidateMeasure();
testObject.AsyncLocalField.Value = null;
});
Assert.True(impl.AskedForSignal);
impl.ExecuteSignal();
Assert.Null(test);
}
[Fact]
public void MediaContextRenderSchedulingAllowsAlreadySuppressedExecutionContextFlow()
{
var impl = new SimpleDispatcherWithBackgroundProcessingImpl();
using var services = new DispatcherServices(impl);
Dispatcher.InitializeUIThreadDispatcher(impl);
var testObject = new AsyncLocalTestClass();
string? test = "Not measured";
Exception? schedulingException = null;
var control = new AsyncLocalMeasureControl(() => testObject.AsyncLocalField.Value, value => test = value);
var root = new TestRoot { Child = control };
root.ExecuteInitialLayoutPass();
control.RecordMeasure = true;
Dispatcher.UIThread.Post(() =>
{
testObject.AsyncLocalField.Value = "Initial Value";
try
{
using (ExecutionContext.SuppressFlow())
{
control.InvalidateMeasure();
}
}
catch (Exception e)
{
schedulingException = e;
}
testObject.AsyncLocalField.Value = null;
});
Assert.True(impl.AskedForSignal);
impl.ExecuteSignal();
Assert.Null(schedulingException);
Assert.Null(test);
}
[Fact] [Fact]
public async Task ExecutionContextCultureInfoIsPreservedInDispatcherInvokeAsync() public async Task ExecutionContextCultureInfoIsPreservedInDispatcherInvokeAsync()
{ {

Loading…
Cancel
Save