Skip to content

Commit

Permalink
Merge pull request #18899 from unoplatform/dev/jela/brush-weak
Browse files Browse the repository at this point in the history
perf(brush): Don't use reflection to invoke brush updates
  • Loading branch information
jeromelaban authored Nov 27, 2024
2 parents facfb7f + 63ddc19 commit 8bae473
Show file tree
Hide file tree
Showing 26 changed files with 515 additions and 260 deletions.
2 changes: 1 addition & 1 deletion build/ci/.azure-devops-wasm-uitests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
- task: CopyFiles@2
displayName: 'Publish Wasm Site (net9.0)'
inputs:
SourceFolder: $(build.sourcesdirectory)/src/SamplesApp/SamplesApp.Wasm/bin/Release/net9.0/browser-wasm/publish/wwwroot
SourceFolder: $(build.sourcesdirectory)/src/SamplesApp/SamplesApp.Wasm/bin/Release/net9.0/publish/wwwroot
Contents: '**/*.*'
TargetFolder: $(build.artifactstagingdirectory)/site-net9.0-$(XAML_FLAVOR_BUILD)
CleanTargetFolder: false
Expand Down
33 changes: 21 additions & 12 deletions doc/articles/uno-development/Internal-WeakEventHelper.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The WeakEventHelper class is an internal method that is designed to provide a
memory-friendly environment for registering to events internally in Uno.

This class is not exposed to the end-user because its patterns do not fit with the
original UWP event-based designs of the API.
original WinUI event-based designs of the API.

## The RegisterEvent method

Expand All @@ -17,35 +17,44 @@ that both the source and the target are weak. The source must be kept alive by
another longer-lived reference, and the target is kept alive by the
return disposable.

If the returned disposable is collected, the handler will also be
collected. Conversely, if the provided list is collected
raising the event will produce nothing.
If the provided handler is collected, the registration will
be collected as well. The returned disposable is not tracked, which means that it will
not remove the registration when collected, unless the provided handler is a lambda. In
this case, the lambda's lifetime is tied to the returned disposable.

The WeakEventCollection automatically manages its internal registration list using GC events.

Here's a usage example:

private List<WeakEventHelper.GenericEventHandler> _sizeChangedHandlers = new List<WeakEventHelper.GenericEventHandler>();
```csharp
private WeakEventHelper.WeakEventCollection? _sizeChangedHandlers;

internal IDisposable RegisterSizeChangedEvent(WindowSizeChangedEventHandler handler)
{
return WeakEventHelper.RegisterEvent(
_sizeChangedHandlers,
_sizeChangedHandlers ??= new(),
handler,
(h, s, e) => (h as WindowSizeChangedEventHandler)?.Invoke(s, (WindowSizeChangedEventArgs)e)
);
}

internal void RaiseEvent()
{
_sizeChangedHandlers?.Invoke(this, new WindowSizeChangedEventArgs());
}
```

The RegisterEvent method is intentionally non-generic to avoid the cost related to AOT performance. The
performance cost is shifted to downcast and upcast checks in the `EventRaiseHandler` handlers.

The returned disposable must be used as follows :

private SerialDisposable _sizeChangedSubscription = new SerialDisposable();
```csharp
private IDisposable? _sizeChangedSubscription;

...

_sizeChangedSubscription.Disposable = null;
_sizeChangedSubscription?.Dispose();

if (Owner != null)
{
_sizeChangedSubscription.Disposable = Window.Current.RegisterSizeChangedEvent(OnCurrentWindowSizeChanged);
}
_sizeChangedSubscription = Window.Current.RegisterSizeChangedEvent(OnCurrentWindowSizeChanged);
```
1 change: 1 addition & 0 deletions src/SamplesApp/SamplesApp.Wasm/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static void Main(string[] args)
#endif

Microsoft.UI.Xaml.Application.Start(_ => _app = new App());

}
}
}
3 changes: 3 additions & 0 deletions src/SamplesApp/SamplesApp.Wasm/SamplesApp.Wasm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
<WasmShellIncludeWindowsCompatibility>false</WasmShellIncludeWindowsCompatibility>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

<!-- Required for net9 workloads -->
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>

<!-- To build with AOT, uncomment these two lines -->
<!--<WasmShellEnableEmccProfiling>true</WasmShellEnableEmccProfiling>
<WasmShellMonoRuntimeExecutionMode>InterpreterAndAOT</WasmShellMonoRuntimeExecutionMode>-->
Expand Down
237 changes: 237 additions & 0 deletions src/Uno.UI.RuntimeTests/Tests/Windows_UI/Given_WeakEventHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#if HAS_UNO
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Uno.Buffers;
using Windows.Graphics.Capture;
using Windows.UI.Core;

namespace Uno.UI.RuntimeTests.Tests.Windows_UI;

[TestClass]
public class Given_WeakEventHelper
{
[TestMethod]
public void When_Explicit_Dispose()
{
WeakEventHelper.WeakEventCollection SUT = new();

var invoked = 0;
Action action = () => invoked++;

var disposable = WeakEventHelper.RegisterEvent(SUT, action, (s, e, a) => (s as Action).Invoke());

SUT.Invoke(this, null);

Assert.AreEqual(1, invoked);

disposable.Dispose();

// When disposed invoking events won't call the original action
// the registration has been disposed.
SUT.Invoke(this, null);

Assert.AreEqual(1, invoked);
}

[TestMethod]
public void When_Registration_Collected()
{
WeakEventHelper.WeakEventCollection SUT = new();

var invoked = 0;
Action action = () => invoked++;

void Do()
{
var disposable = WeakEventHelper.RegisterEvent(SUT, action, (s, e, a) => (s as Action).Invoke());

SUT.Invoke(this, null);

Assert.AreEqual(1, invoked);

disposable = null;
}

Do();

GC.Collect(2);
GC.WaitForPendingFinalizers();

// Even if the disposable is collected, the event should still be invoked
// as the disposable does not track the event registration.
SUT.Invoke(this, null);

Assert.AreEqual(2, invoked);
}

[TestMethod]
public void When_Target_Collected()
{
WeakEventHelper.WeakEventCollection SUT = new();

var invoked = 0;
IDisposable disposable = null;

void Do()
{
Action action = () => invoked++;

disposable = WeakEventHelper.RegisterEvent(SUT, action, (s, e, a) => (s as Action).Invoke());

SUT.Invoke(this, null);

Assert.AreEqual(1, invoked);
}

Do();

GC.Collect(2);
GC.WaitForPendingFinalizers();

SUT.Invoke(this, null);

Assert.AreEqual(2, invoked);
}

[TestMethod]
public void When_Many_Targets_Collected()
{
WeakEventHelper.WeakEventCollection SUT = new();

var invoked = 0;
List<IDisposable> disposable = new();

void Do()
{
Action action = () => invoked++;

disposable.Add(WeakEventHelper.RegisterEvent(SUT, action, (s, e, a) => (s as Action).Invoke()));

SUT.Invoke(this, null);
}

for (int i = 0; i < 100; i++)
{
Do();
}

SUT.Invoke(this, null);

Assert.AreEqual(5150, invoked);

disposable.Clear();

GC.Collect(2);
GC.WaitForPendingFinalizers();

// Ensure that everything has been collected.
SUT.Invoke(this, null);

Assert.AreEqual(5150, invoked);
}

[TestMethod]
public void When_Collection_Disposed()
{
WeakEventHelper.WeakEventCollection SUT = new();

var invoked = 0;

Action action = () => invoked++;

var disposable = WeakEventHelper.RegisterEvent(SUT, action, (s, e, a) => (s as Action).Invoke());

SUT.Invoke(this, null);

Assert.AreEqual(1, invoked);

SUT.Dispose();
}

[TestMethod]
public async Task When_Collection_Collected()
{
WeakReference actionRef = null;
WeakReference collectionRef = null;

void Do()
{
WeakEventHelper.WeakEventCollection SUT = new();
collectionRef = new(SUT);

var invoked = 0;

Action action = () => invoked++;
actionRef = new(actionRef);

var disposable = WeakEventHelper.RegisterEvent(SUT, action, (s, e, a) => (s as Action).Invoke());

SUT.Invoke(this, null);

Assert.AreEqual(1, invoked);

SUT.Dispose();
}

Do();

var sw = Stopwatch.StartNew();

while ((actionRef.IsAlive || collectionRef.IsAlive) && sw.ElapsedMilliseconds < 5000)
{
await Task.Delay(10);
GC.Collect(2);
GC.WaitForPendingFinalizers();
}

Assert.IsFalse(actionRef.IsAlive);
Assert.IsFalse(collectionRef.IsAlive);
}

[TestMethod]
public void When_Empty_Trim_Stops()
{
TestPlatformProvider trimProvider = new();
WeakEventHelper.WeakEventCollection SUT = new(trimProvider);

var invoked = 0;

Action action = () => invoked++;

Assert.IsNull(trimProvider.Invoke());

var disposable = WeakEventHelper.RegisterEvent(SUT, action, (s, e, a) => (s as Action).Invoke());

Assert.IsTrue(trimProvider.Invoke());

SUT.Invoke(this, null);

Assert.AreEqual(1, invoked);

disposable.Dispose();

Assert.IsFalse(trimProvider.Invoke());

Assert.AreEqual(1, invoked);
}

private class TestPlatformProvider : WeakEventHelper.ITrimProvider
{
private object _target;
private Func<object, bool> _callback;

public void RegisterTrimCallback(Func<object, bool> callback, object target)
{
_target = target;
_callback = callback;
}

public bool? Invoke() => _callback?.Invoke(_target);
}
}
#endif
Loading

0 comments on commit 8bae473

Please sign in to comment.