Skip to content

Commit

Permalink
Merge pull request #18941 from unoplatform/mergify/bp/release/stable/…
Browse files Browse the repository at this point in the history
…5.4/pr-18899

perf(brush): Don't use reflection to invoke brush updates (backport #18899)
  • Loading branch information
jeromelaban authored Nov 27, 2024
2 parents 730cde1 + 3a65079 commit 9734e57
Show file tree
Hide file tree
Showing 25 changed files with 520 additions and 259 deletions.
9 changes: 9 additions & 0 deletions build/ci/.azure-devops-project-template-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ jobs:
dependsOn:
- Generate_Packages

variables:
NuGetAudit: false

steps:
- task: DownloadBuildArtifacts@0
inputs:
Expand Down Expand Up @@ -55,6 +58,9 @@ jobs:
dependsOn:
- Generate_Packages

variables:
NuGetAudit: false

steps:
- task: DownloadBuildArtifacts@0
inputs:
Expand Down Expand Up @@ -95,6 +101,9 @@ jobs:

container: unoplatform/wasm-build:2.3

variables:
NuGetAudit: false

steps:
- task: DownloadBuildArtifacts@0
inputs:
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 @@ -19,6 +19,7 @@ public static void Main(string[] args)
#endif

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

}
}
}
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 9734e57

Please sign in to comment.