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

CollectionView Windows Memory Leak, Android Laggy scrolling #13557

Closed
alfamizar opened this issue Feb 25, 2023 · 7 comments · Fixed by #13656
Closed

CollectionView Windows Memory Leak, Android Laggy scrolling #13557

alfamizar opened this issue Feb 25, 2023 · 7 comments · Fixed by #13656
Assignees
Labels
area-controls-collectionview CollectionView, CarouselView, IndicatorView fixed-in-8.0.0-preview.3.8149 Look for this fix in 8.0.0-preview.3.8149! platform/android 🤖 t/bug Something isn't working t/perf The issue affects performance (runtime speed, memory usage, startup time, etc.)

Comments

@alfamizar
Copy link

alfamizar commented Feb 25, 2023

Description

I noticed huge memory leak in my app, but seems this is widely spread issue, as it is easy to reproduce even in Monkey Finder
https://github.com/dotnet-presentations/dotnet-maui-workshop.git

image

Also, scrolling performance is bad on Samsung Galaxy S20 Ultra (12GB RAM) if Data Tepmplate contains Frame, without wrapping with a Grid. (I mean fixing Memory leak on Windows, ruins scrolling performance (by potentional memory) on Android)

Steps to Reproduce

  1. Run Monkey finder on Windows, in release build, link
  2. Click Get Monkeys and see, how memory usage increases up to infinity

Link to public reproduction project repository

https://github.com/dotnet-presentations/dotnet-maui-workshop.git

Version with bug

7.0 (current)

Last version that worked well


Affected platforms

Android, Windows

Affected platform versions

Android 9, 12; Windows 11

Did you find any workaround?

Workaround for Windows memory leak is simple. Remove Grid, that wraps Frame
image

Using this fix for Windows works fine
image
But reduces scrolling performance on Android, and may be causing memory leak (I can not check it for sure)

Relevant log output

No response

@alfamizar alfamizar added the t/bug Something isn't working label Feb 25, 2023
@Eilon Eilon added area-controls-collectionview CollectionView, CarouselView, IndicatorView legacy-area-perf Startup / Runtime performance labels Feb 27, 2023
@rachelkang
Copy link
Member

@jonathanpeppers

@ghost
Copy link

ghost commented Feb 28, 2023

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@jonathanpeppers
Copy link
Member

I debugged this app quite a bit today, I believe there are two core problems here:

  1. Something XAML Hot Reload causes. Things worked better if I do:

image

I will investigate this further and let the right team know.

  1. VisualElement.Background subscribes to events, which is a problem if the Background comes from App.Resources:

void NotifyBackgroundChanges()
{
if (Background is ImmutableBrush)
return;
if (Background != null)
{
Background.Parent = this;
Background.PropertyChanged += OnBackgroundChanged;
if (Background is GradientBrush gradientBrush)
gradientBrush.InvalidateGradientBrushRequested += InvalidateGradientBrushRequested;
}
}

Can be reproduced in a test:

[Theory]
[InlineData(typeof(ImmutableBrush), false)]
[InlineData(typeof(SolidColorBrush), false)]
[InlineData(typeof(LinearGradientBrush), true)]
[InlineData(typeof(RadialGradientBrush), true)]
public async Task BackgroundDoesNotLeak(Type type, bool defaultCtor)
{
	var brush = defaultCtor ?
		(Brush)Activator.CreateInstance(type) :
		(Brush)Activator.CreateInstance(type, Colors.CornflowerBlue);

	var reference = new WeakReference(new VisualElement { Background = brush });

	await Task.Yield();
	GC.Collect();
	GC.WaitForPendingFinalizers();

	Assert.False(reference.IsAlive, "VisualElement should not be alive!");
}

Only ImmutableBrush passes, as it is special cased and returns early.

I'll keep working on this. 👍

@alfamizar
Copy link
Author

alfamizar commented Mar 2, 2023

@jonathanpeppers hi. Thanks for digging into it.

Regarding first problem, I am surprised, as I didn't expect that hot reload could cause issues in Release mode.

@jonathanpeppers
Copy link
Member

jonathanpeppers commented Mar 2, 2023

I was testing in Debug mode, so I could do things like put breakpoints in MAUI.

jonathanpeppers added a commit to jonathanpeppers/maui that referenced this issue Mar 2, 2023
Fixes: dotnet#12344
Fixes: dotnet#13557
Context: https://github.com/dotnet-presentations/dotnet-maui-workshop

While testing the `Monkey Finder` sample, I found the following
scenario causes an issue:

1. Declare a `{StaticResource}` `Brush` at the `Application` level,
   with a lifetime of the entire application.

2. Set `Background` on an item in a `CollectionView`, `ListView`, etc.

3. Scroll a lot, navigate away, etc.

4. The `Brush` will hold onto any `View`'s indefinitely.

The core problem here being `VisualElement` does:

    void NotifyBackgroundChanges()
    {
        if (Background is ImmutableBrush)
            return;

        if (Background != null)
        {
            Background.Parent = this;
            Background.PropertyChanged += OnBackgroundChanged;

            if (Background is GradientBrush gradientBrush)
                gradientBrush.InvalidateGradientBrushRequested += InvalidateGradientBrushRequested;
        }
    }

If no user code sets `Background` to `null`, these events remain
subscribed.

To fix this:

1. Create a `WeakNotifyCollectionChangedProxy` type for event subscription.

2. Don't set `Background.Parent = this`

~~ General Cleanup ~~

Through doing other fixes related to memory leaks & C# events, we've
started to gain a collection of `WeakEventProxy`-related types.

I created some core `internal` types to be reused:

* `abstract WeakEventProxy<TSource, TEventHandler>`
* `WeakNotifyCollectionChangedProxy`

The following classes now make use of these new shared types:

* `BindingExpression`
* `BindableLayout`
* `ListProxy`
* `VisualElement`

This should hopefully reduce mistakes and reuse code in this area.

~~ Concerns ~~

Since, we are no longer doing:

    Background.Parent = this;

This is certainly a behavior change. It is now replaced with:

    SetInheritedBindingContext(Background, BindingContext);

I had to update one unit test that was asserting `Background.Parent`,
which can assert `Background.BindingContext` instead.

As such, this change is definitely sketchy and I wouldn't backport to
.NET 7 immediately. We might test it out in a .NET 8 preview first.
@jonathanpeppers jonathanpeppers self-assigned this Mar 2, 2023
@alfamizar
Copy link
Author

@jonathanpeppers if you find a workaround, that could fix an issue, before fix will be merged into .NET 7 MAUI, please, let me know.

jonathanpeppers added a commit to jonathanpeppers/maui that referenced this issue Mar 2, 2023
Fixes: dotnet#12344
Fixes: dotnet#13557
Context: https://github.com/dotnet-presentations/dotnet-maui-workshop

While testing the `Monkey Finder` sample, I found the following
scenario causes an issue:

1. Declare a `{StaticResource}` `Brush` at the `Application` level,
   with a lifetime of the entire application.

2. Set `Background` on an item in a `CollectionView`, `ListView`, etc.

3. Scroll a lot, navigate away, etc.

4. The `Brush` will hold onto any `View`'s indefinitely.

The core problem here being `VisualElement` does:

    void NotifyBackgroundChanges()
    {
        if (Background is ImmutableBrush)
            return;

        if (Background != null)
        {
            Background.Parent = this;
            Background.PropertyChanged += OnBackgroundChanged;

            if (Background is GradientBrush gradientBrush)
                gradientBrush.InvalidateGradientBrushRequested += InvalidateGradientBrushRequested;
        }
    }

If no user code sets `Background` to `null`, these events remain
subscribed.

To fix this:

1. Create a `WeakNotifyCollectionChangedProxy` type for event subscription.

2. Don't set `Background.Parent = this`

~~ General Cleanup ~~

Through doing other fixes related to memory leaks & C# events, we've
started to gain a collection of `WeakEventProxy`-related types.

I created some core `internal` types to be reused:

* `abstract WeakEventProxy<TSource, TEventHandler>`
* `WeakNotifyCollectionChangedProxy`

The following classes now make use of these new shared types:

* `BindingExpression`
* `BindableLayout`
* `ListProxy`
* `VisualElement`

This should hopefully reduce mistakes and reuse code in this area.

~~ Concerns ~~

Since, we are no longer doing:

    Background.Parent = this;

This is certainly a behavior change. It is now replaced with:

    SetInheritedBindingContext(Background, BindingContext);

I had to update one unit test that was asserting `Background.Parent`,
which can assert `Background.BindingContext` instead.

As such, this change is definitely sketchy and I wouldn't backport to
.NET 7 immediately. We might test it out in a .NET 8 preview first.
@jonathanpeppers
Copy link
Member

jonathanpeppers commented Mar 2, 2023

@alfamizar in the example above, you could avoid using {StaticResource} including Style, Brush, etc. in a DataTemplate? Basically just hardcode values instead.

@ghost ghost locked as resolved and limited conversation to collaborators Apr 7, 2023
@samhouts samhouts modified the milestones: Backlog, .NET 8 May 22, 2023
@samhouts samhouts added the fixed-in-8.0.0-preview.3.8149 Look for this fix in 8.0.0-preview.3.8149! label Jun 8, 2023
@Eilon Eilon added t/perf The issue affects performance (runtime speed, memory usage, startup time, etc.) and removed legacy-area-perf Startup / Runtime performance labels May 10, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-controls-collectionview CollectionView, CarouselView, IndicatorView fixed-in-8.0.0-preview.3.8149 Look for this fix in 8.0.0-preview.3.8149! platform/android 🤖 t/bug Something isn't working t/perf The issue affects performance (runtime speed, memory usage, startup time, etc.)
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants