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

[controls] fix memory leak with CarouselView & INotifyCollectionChanged #18267

Merged

Conversation

jonathanpeppers
Copy link
Member

@jonathanpeppers jonathanpeppers commented Oct 23, 2023

Context: #17726

In investigating #17726, I found a memory leak with CarouselView:

  1. Have a long-lived INotifyCollectionChanged like
    ObservableCollection, that lives the life of the application.

  2. Set CarouselView.ItemsSource to the collection.

  3. CarouselView will live forever, even if the page is popped, etc.

I further expanded upon MemoryTests to assert more things for each
control, and was able to reproduce this issue.

To fully solve this, I had to fix issues on each platform.

Android

ObservableItemsSource subscribes to the INotifyCollectionChanged
event, making it live forever in this case.

To fix this, I used the same technique in 58a42e5:
WeakNotifyCollectionChangedProxy. ObservableItemsSource is used for
ItemsView-like controls, so this may fix other leaks as well.

Windows

Windows has the same issue as Android, but for the following types:

  • CarouselViewHandler subscribes to INotifyCollectionChanged

  • ObservableItemTemplateCollection subscribes to
    INotifyCollectionChanged

These could be fixed with WeakNotifyCollectionChangedProxy.

Then I found another issue with ItemTemplateContext, it holds a strong
reference to BindableObject Container, the CarouselView. I found
this kept the CarouselView alive indefinitely, but changing to a
WeakReference solved this issue.

These three types are used for other ItemsView-like controls, so this
may fix other leaks as well.

iOS/Catalyst

These platforms suffered from multiple circular references:

  • CarouselViewController -> CarouselView -> CarouselViewHandler ->
    CarouselViewController

  • ItemsViewController -> CarouselView -> CarouselViewHandler ->
    ItemsViewController (CarouselViewController subclasses this)

  • CarouselViewLayout -> CarouselView -> CarouselViewHandler ->
    CarouselViewLayout

The analyzer added in 2e65626 did not yet catch these because I only
added the analyzer so far for Microsoft.Maui.dll and these issues were
in Microsoft.Maui.Controls.dll. In a future PR, we can proactively try
to add the analyzer to all projects.

Conclusion

Unfortunately, this does not fully solve #17726, as there is at least
one further leak on Android from my testing. But this is a good step
forward.

@jonathanpeppers jonathanpeppers added the memory-leak 💦 Memory usage grows / objects live forever (sub: perf) label Oct 23, 2023
return new ObservableItemsSource(itemsSource as IEnumerable, container, notifier);
return new ObservableItemsSource(itemsSource, container, notifier);
Copy link
Member Author

Choose a reason for hiding this comment

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

This as was just extra, the itemsSource is already IEnumerable.

Comment on lines +24 to +25
_collectionChanged = CollectionChanged;
_proxy.Subscribe((INotifyCollectionChanged)itemSource, _collectionChanged);
Copy link
Member Author

Choose a reason for hiding this comment

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

We store CollectionChanged in a field, so it will remain alive as long as the ObservableItemsSource.

Details here: https://github.com/dotnet/maui/wiki/Memory-Leaks#when-we-cant-use-weakeventmanager

@jonathanpeppers jonathanpeppers added this to the .NET 8 + Servicing milestone Oct 23, 2023
@jonathanpeppers
Copy link
Member Author

The new test fails on iOS & Windows, looking into it.

@jsuarezruiz jsuarezruiz added the area-controls-collectionview CollectionView, CarouselView, IndicatorView label Oct 24, 2023
@jonathanpeppers
Copy link
Member Author

This one ended up being a big mess: different problems on each platform...

If this is green, I'll rebase and explain in a better PR description/commit message.

…anged`

Context: dotnet#17726

In investigating dotnet#17726, I found a memory leak with `CarouselView`:

1. Have a long-lived `INotifyCollectionChanged` like
   `ObservableCollection`, that lives the life of the application.

2. Set `CarouselView.ItemsSource` to the collection.

3. `CarouselView` will live forever, even if the page is popped, etc.

I further expanded upon `MemoryTests` to assert more things for each
control, and was able to reproduce this issue.

To fully solve this, I had to fix issues on each platform.

~~ Android ~~

`ObservableItemsSource` subscribes to the `INotifyCollectionChanged`
event, making it live forever in this case.

To fix this, I used the same technique in 58a42e5:
`WeakNotifyCollectionChangedProxy`. `ObservableItemsSource` is used for
`ItemsView`-like controls, so this may fix other leaks as well.

~~ Windows ~~

Windows has the same issue as Android, but for the following types:

* `CarouselViewHandler` subscribes to `INotifyCollectionChanged`

* `ObservableItemTemplateCollection` subscribes to
  `INotifyCollectionChanged`

These could be fixed with `WeakNotifyCollectionChangedProxy`.

Then I found another issue with `ItemTemplateContext`

These three types are used for other `ItemsView`-like controls, so this
may fix other leaks as well.

~~ iOS/Catalyst ~~

These platforms suffered from multiple circular references:

* `CarouselViewController` -> `CarouselView` -> `CarouselViewHandler` ->
  `CarouselViewController`

* `ItemsViewController` -> `CarouselView` -> `CarouselViewHandler` ->
  `ItemsViewController` (`CarouselViewController` subclasses this)

* `CarouselViewLayout` -> `CarouselView` -> `CarouselViewHandler` ->
  `CarouselViewLayout`

The analyzer added in 2e65626 did not yet catch these because I only
added the analyzer so far for `Microsoft.Maui.dll` and these issues were
in `Microsoft.Maui.Controls.dll`. In a future PR, we can proactively try
to add the analyzer to all projects.

~~ Conclusion ~~

Unfortunately, this does not fully solve dotnet#17726, as there is at least
one further leak on Android from my testing. But this is a good step
forward.
@jonathanpeppers jonathanpeppers force-pushed the INotifyCollectionChangedLeak branch from fdb2bef to b1d8176 Compare October 24, 2023 20:59
@jonathanpeppers jonathanpeppers changed the title [core] fix memory leak with CarouselView & INotifyCollectionChanged [controls] fix memory leak with CarouselView & INotifyCollectionChanged Oct 24, 2023
@jonathanpeppers jonathanpeppers marked this pull request as ready for review October 24, 2023 21:02
@jonathanpeppers jonathanpeppers requested a review from a team as a code owner October 24, 2023 21:02
@jonathanpeppers jonathanpeppers merged commit 6f073d6 into dotnet:main Nov 7, 2023
@jonathanpeppers jonathanpeppers deleted the INotifyCollectionChangedLeak branch November 7, 2023 00:23
@github-actions github-actions bot locked and limited conversation to collaborators Dec 7, 2023
@samhouts samhouts added the fixed-in-8.0.6 Look for this fix in 8.0.6 SR1! label Aug 2, 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.6 Look for this fix in 8.0.6 SR1! memory-leak 💦 Memory usage grows / objects live forever (sub: perf)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants