Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Xiaoy312
Copy link
Contributor

@Xiaoy312 Xiaoy312 commented Dec 17, 2025

GitHub Issue: closes unoplatform/uno-private#1670

PR Type:

  • 🐞 Bugfix
  • πŸ”„ Refactoring (no functional changes, no api changes)

What is the current behavior? πŸ€”

  1. FrameworkTemplate can leak if its view-factory method isn't static
  2. FrameworkTemplatePool still creates an entry for every data-template even if it is disabled

What is the new behavior? πŸš€

  1. FrameworkTemplate nows holds its view-factory with the new weak-delegate
  2. FrameworkTemplatePool wont create any entry when disabled

PR Checklist βœ…

Please check if your PR fulfills the following requirements:

Other information ℹ️

view-factory method are usually generated with static modifier, except when hot-reload is enable (which implies in DEBUG only)

Copilot AI review requested due to automatic review settings December 17, 2025 03:14
@Xiaoy312
Copy link
Contributor Author

todo: tests

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses memory leaks in the Uno Platform's template system by introducing weak references for delegate storage and optimizing template pooling. The changes prevent FrameworkTemplate instances from keeping view factories alive unnecessarily and avoid creating unnecessary pool entries when pooling is disabled.

Key Changes:

  • Introduces a new WeakDelegate<TDelegate> wrapper class to hold delegates with weak references to their targets
  • Updates FrameworkTemplate to store its view factory using WeakDelegate instead of direct delegate references
  • Optimizes FrameworkTemplatePool to skip creating pool entries when pooling is disabled

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/Uno.UI/Helpers/WeakDelegate.cs New helper class implementing weak delegate pattern to prevent memory leaks by holding delegate targets weakly
src/Uno.UI/UI/Xaml/FrameworkTemplate.cs Updates view factory storage to use WeakDelegate wrapper and fixes typo in XAML scope comment
src/Uno.UI/UI/Xaml/FrameworkTemplatePool.cs Refactors pool management to use TryGetTemplatePool and skip pool entry creation when pooling is disabled, plus fixes error message typo

Critical Issues Found:

  1. Inverted logic in TryGetTemplatePool at line 320 - returns null when pooling IS enabled instead of when disabled
  2. Non-existent API - HasSingleTarget doesn't exist on the Delegate class; should use GetInvocationList().Length == 1
  3. Missing issue reference in PR description - the PR must link to a GitHub issue per repository guidelines

@unodevops
Copy link
Contributor

πŸ€– Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-22161/wasm-skia-net9/index.html

@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22161/docs/index.html

@unodevops
Copy link
Contributor

⚠️⚠️ The build 188627 has failed on Uno.UI - CI.

public TDelegate? Delegate => Instance switch
{
// for instanced method delegate, only try to create if the instance reference is still alive
{ Target: { } t } => System.Delegate.CreateDelegate(typeof(TDelegate), t, Method),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are re-creating delegate on each access ... we need to validate the performance of this! (Not sure if the CreateDelegate involves reflection).

Copy link
Contributor Author

@Xiaoy312 Xiaoy312 Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invoking a delegate is quite fast, almost comparable to direct call
that said, since we are rebuilding it on every invocation, there is a concern here
it would argue that, for its current sole usage, template builder, the bulk of work would be in the builder itself.
and for now, we can rest assured that this wont impact RELEASE with static delegate being cached

@Xiaoy312 Xiaoy312 force-pushed the dev/xygu/20251216/hotreload-fwktemplate-leak branch from 32fa54f to c158701 Compare December 17, 2025 16:38
@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22161/docs/index.html

@unodevops
Copy link
Contributor

⚠️⚠️ The build 188789 has failed on Uno.UI - CI.

@Xiaoy312 Xiaoy312 added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Dec 17, 2025
Copilot AI review requested due to automatic review settings December 19, 2025 00:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

/// </remarks>
public TDelegate? Delegate =>
_staticDelegate ??
// for instanced method delegate, only try to create if the instance reference is still alive
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "for instanced method delegate" but should use the standard terminology "instance method delegate" (without the 'd').

Suggested change
// for instanced method delegate, only try to create if the instance reference is still alive
// for instance method delegate, only try to create if the instance reference is still alive

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 103
ο»Ώ#nullable enable

using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace Uno.UI.Helpers;

internal interface IDelegate<TDelegate> where TDelegate : Delegate
{
public object? Target { get; }
public MethodInfo Method { get; }
public TDelegate? Delegate { get; }
}

/// <summary>
/// Represents a plain wrapper over a normal delegate instance.
/// </summary>
/// <remarks>
/// Solely used to conform to the <see cref="IDelegate{TDelegate}"/> interface,
/// so <see cref="WeakDelegate{TDelegate}"/> and literal delegate can be used interchangeably.
/// </remarks>
/// <typeparam name="TDelegate">The type of delegate encapsulated by this instance. Must derive from <see cref="Delegate"/>.</typeparam>
internal class LiteralDelegate<TDelegate> : IDelegate<TDelegate>
where TDelegate : Delegate
{
public object? Target => Delegate!.Target;
public MethodInfo Method => Delegate!.Method;
public TDelegate? Delegate { get; }

public LiteralDelegate(TDelegate d)
{
Delegate = d;
}
}

/// <summary>
/// Represents a delegate that references its <see cref="Delegate.Target"/> weakly, allowing the target to be eventually garbage collected.
/// </summary>
/// <remarks>Use <see cref="WeakDelegate{TDelegate}"/> to hold a reference to a delegate without preventing its
/// target object from being collected by the garbage collector. This is useful for scenarios where you want to
/// avoid memory leaks caused by strong references. Only delegates with a single target are supported;
/// multicast delegates are not allowed.</remarks>
/// <typeparam name="TDelegate">The type of delegate to be referenced. Must derive from <see cref="Delegate"/>.</typeparam>
internal class WeakDelegate<TDelegate> : IDelegate<TDelegate>
where TDelegate : Delegate
{
public WeakReference? Instance { get; init; }
public MethodInfo Method { get; init; }

// static method delegate doesn't capture any target instance, so we can just reuse it
private TDelegate? _staticDelegate;

public WeakDelegate(TDelegate d)
{
if (!d.HasSingleTarget)
{
throw new NotImplementedException("Multi-cast delegate not supported");
}

if (d.Target is { } t)
{
Instance = new WeakReference(t);
Method = d.Method;
}
else
{
_staticDelegate = d;
Method = d.Method; // still used outside of this class for logging
}
}

public object? Target => Instance?.Target;

/// <summary>
/// Gets the delegate instance that targets the referenced object and method,
/// may be null if the target is no longer available.
/// </summary>
/// <remarks>
/// DO NOT store/cache the returned delegate, as it keeps a strong reference to the target just like the original delegate.
/// </remarks>
public TDelegate? Delegate =>
_staticDelegate ??
// for instanced method delegate, only try to create if the instance reference is still alive
(Instance!.Target is { } t
? System.Delegate.CreateDelegate(typeof(TDelegate), t, Method) as TDelegate
: null);
}

internal class DelegateHelper
{
public static LiteralDelegate<TDelegate> CreateLiteral<TDelegate>(TDelegate d)
where TDelegate : Delegate
{
return new(d);
}

public static WeakDelegate<TDelegate> CreateWeak<TDelegate>(TDelegate d)
where TDelegate : Delegate
{
return new(d);
}
}
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new WeakDelegate class lacks test coverage. Given that this is a critical component for preventing memory leaks and involves complex logic around weak references, delegate recreation, and handling both static and instance methods, consider adding unit tests to verify its behavior, especially edge cases like delegate recreation after garbage collection and proper handling of static vs instance delegates.

Copilot uses AI. Check for mistakes.
// But if the factory doesn't have a target (no capture), then we use the literal delegate that has no overhead.
var inner = factory.Target is not null
? DelegateHelper.CreateWeak(factory)
: DelegateHelper.CreateLiteral(factory) as IDelegate<TDelegate>;
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast as IDelegate<TDelegate> is redundant and potentially misleading. DelegateHelper.CreateLiteral(factory) always returns LiteralDelegate<TDelegate> which implements IDelegate<TDelegate>, so the cast will never fail. Consider removing the cast or assigning the result directly to inner without the cast.

Suggested change
: DelegateHelper.CreateLiteral(factory) as IDelegate<TDelegate>;
: DelegateHelper.CreateLiteral(factory);

Copilot uses AI. Check for mistakes.
private readonly static DataTemplate InnerContentPresenterTemplate =
new DataTemplate(owner: null, factory: InnerContentPresenterTemplateImpl);

// note: Lambda functions even with `static` and none of variable captures
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammatical error in comment: "none of variable captures" should be "no variable captures" or "none of the variable captures".

Suggested change
// note: Lambda functions even with `static` and none of variable captures
// note: Lambda functions even with `static` and no variable captures

Copilot uses AI. Check for mistakes.

public WeakDelegate(TDelegate d)
{
if (!d.HasSingleTarget)
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The property HasSingleTarget does not exist on the Delegate type in .NET. To check if a delegate is multicast, use GetInvocationList().Length == 1 instead.

Suggested change
if (!d.HasSingleTarget)
if (d.GetInvocationList().Length != 1)

Copilot uses AI. Check for mistakes.
{
if (!d.HasSingleTarget)
{
throw new NotImplementedException("Multi-cast delegate not supported");
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception message says "Multi-cast delegate not supported" but uses NotImplementedException. For unsupported operations, use NotSupportedException instead, which is the standard exception type for operations that are not supported by design.

Suggested change
throw new NotImplementedException("Multi-cast delegate not supported");
throw new NotSupportedException("Multi-cast delegate not supported");

Copilot uses AI. Check for mistakes.
public TDelegate? Delegate =>
_staticDelegate ??
// for instanced method delegate, only try to create if the instance reference is still alive
(Instance!.Target is { } t
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference issue: Instance!.Target may throw NullReferenceException if Instance is null. This can occur when the delegate is static (has no target), in which case Instance is never initialized. Add a null check: Instance?.Target instead of Instance!.Target.

Suggested change
(Instance!.Target is { } t
(Instance?.Target is { } t

Copilot uses AI. Check for mistakes.
@unodevops
Copy link
Contributor

πŸ€– Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-22161/wasm-skia-net9/index.html

@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22161/docs/index.html

@unodevops
Copy link
Contributor

πŸ€– Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-22161/wasm-skia-net9/index.html

@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22161/docs/index.html

@unodevops
Copy link
Contributor

⚠️⚠️ The build 189150 has failed on Uno.UI - CI.

Copilot AI review requested due to automatic review settings December 19, 2025 08:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment on lines +119 to +122
// We want to keep a weak reference to the factory target, so that templates do not prevent those objects from being GC'd.
// But only so, if the target is not an instance of the closure class, which we use IWeakReferenceProvider to determine since
// the known top-level xaml classes (Pages, ResourceDictionary,...) usually implement this interface.
// If the factory doesn't have a target (no capture), then we use the literal delegate that has no overhead.
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment explaining the weak reference logic is somewhat confusing. Consider rephrasing for clarity. The current logic is: when the target implements IWeakReferenceProvider (indicating it's a top-level XAML class like Page or ResourceDictionary), use a weak reference to avoid memory leaks. When the target doesn't implement this interface (indicating it's likely a compiler-generated closure class), use a literal (strong) reference to keep the closure alive. The phrase "if the target is not an instance of the closure class" could be clearer as "if the target is a top-level XAML class (not a closure)".

Suggested change
// We want to keep a weak reference to the factory target, so that templates do not prevent those objects from being GC'd.
// But only so, if the target is not an instance of the closure class, which we use IWeakReferenceProvider to determine since
// the known top-level xaml classes (Pages, ResourceDictionary,...) usually implement this interface.
// If the factory doesn't have a target (no capture), then we use the literal delegate that has no overhead.
// When the factory target is a top-level XAML class (e.g. Page, ResourceDictionary) that implements IWeakReferenceProvider,
// we wrap the delegate in a weak reference so that the template does not keep that object alive and cause memory leaks.
// When the target does not implement IWeakReferenceProvider (typically a compiler-generated closure class), we keep a strong
// (literal) reference so the closure stays alive. If the factory doesn't have a target (no capture), we also use the literal
// delegate without additional overhead.

Copilot uses AI. Check for mistakes.
Comment on lines 279 to 286
// Same target method (instance or static) (possible if the delegate was created from a
// method group, which are *not* cached by the C# compiler (required by
// the C# spec as of version 6.0)
|| (
ReferenceEquals(left?._viewFactory?.Target, right?._viewFactory?.Target)
&& left?._viewFactory?.Method == right?._viewFactory?.Method
);
|| (left?._hashCode == right?._hashCode)
;

public int GetHashCode(FrameworkTemplate obj) => obj._hashCode;
}
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment about "Same delegate" is misleading with the new implementation. Since _viewFactory is now wrapped in IDelegate<T> instances, the reference equality check will always fail (each template gets its own wrapper instance). The equality now relies solely on the hashCode comparison. Consider updating the comment to reflect this change, or remove the _viewFactory comparison entirely since it will never match.

Copilot uses AI. Check for mistakes.
ο»Ώ#nullable enable

using System;
using System.Diagnostics.CodeAnalysis;
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The System.Diagnostics.CodeAnalysis namespace is imported but not used. Consider removing this unused import.

Suggested change
using System.Diagnostics.CodeAnalysis;

Copilot uses AI. Check for mistakes.
@unodevops
Copy link
Contributor

πŸ€– Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-22161/wasm-skia-net9/index.html

@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22161/docs/index.html

@unodevops
Copy link
Contributor

⚠️⚠️ The build 189168 has failed on Uno.UI - CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants