Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(brush): Don't use reflection to invoke brush updates #18899

Merged
merged 14 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
jeromelaban marked this conversation as resolved.
Show resolved Hide resolved

<!-- 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
Loading