-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
[ios/catalyst] fix memory leaks in ListView #22007
Merged
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Fixes: dotnet#20025 I could reproduce the leak here, pretty easily with changes to `MemoryTests`. `ListView` has several cycles that prevent garbage collection from happening: * `FormsUITableViewController` has `ListView _list` * `FormsUITableViewController` -> `ListView` -> `ListViewRenderer` -> `FormsUITableViewController` * `ListViewDataSource` has `UITableView _uiTableView` * `ListViewDataSource` -> `UITableView` -> `ListViewDataSource` * `ListViewDataSource` has `FormsUITableViewController _uiTableViewController` * `ListViewDataSource` -> `FormsUITableViewController` -> `UITableView` -> `ListViewDataSource` * `ListViewDataSource` has `protected ListView List` * `ListViewDataSource` -> `ListView` -> `ListViewRenderer` -> `FormsUITableViewController` -> `UITableView` -> `ListViewDataSource` I changed the above fields to all be `WeakReference<T>` and the leaks went away.
jonathanpeppers
added
platform/iOS 🍎
memory-leak 💦
Memory usage grows / objects live forever (sub: perf)
labels
Apr 23, 2024
jonathanpeppers
commented
Apr 23, 2024
Context: dotnet#18757
jonathanpeppers
added a commit
to jonathanpeppers/xamarin-android
that referenced
this pull request
Apr 24, 2024
Fixes: dotnet/maui#18757 Context: dotnet/maui#22007 Context: https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/view/View.java;l=19612 If you do something like: new View(myContext).Post(() => { // do something }); If the `View` is never added to the `Window` (`IsAttachedToWindow` is false), the `Runnable` will never be executed, with data stored in this dictionary: static Dictionary<Action, RunnableImplementor> instances = new Dictionary<Action, RunnableImplementor> (); This is a problem if the `Action`: * Is an instance method, will cause the `Action.Target` to live forever * Is an anonymous method with captured variables, will cause the captured variables to live forever I could observe this behavior in a MAUI unit test that: * Creates a `ListView` * Creates the platform view that implements `ListView` * Never adds any of these objects to the `Window` * Makes sure none of the things leak -- *this fails* This seems less likely to occur in a real application, but it is still a bug.
This reverts commit b294779.
rmarinho
approved these changes
Apr 24, 2024
jonpryor
pushed a commit
to dotnet/android
that referenced
this pull request
Apr 25, 2024
Context: dotnet/maui#18757 Context: dotnet/maui#22007 Context: https://github.com/dotnet/maui/blob/440fa7f6ff5602e9cbe23249df5d13c45fb261e1/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/ListViewRenderer.cs#L484-L491 Context: xamarin/monodroid@f4ffb99 Context: 5777337 [`android.view.View.post(Runnable)`][0] adds a `Runnable` callback to the message queue, to be later run on the UI thread. Commit xamarin/monodroid@f4ffb99f, imported in 5777337, added a `View.Post(Action)` overload for this method to make it easier to use. There is also a [`View.removeCallbacks(Runnable)`][1] method which allows removing the specified callback from the message queue. A `View.RemoveCallbacks(Action)` method was also added, permitting: Action a = () => …; View v = new View(context); v.Post(a); v.RemoveCallbacks(a); To make this work, we needed a way look up the `java.lang.Runnable` that corresponds to a given `Action`. Enter `RunnableImplementor` and `RunnableImplementor.instances`: namespace Java.Lang; partial class Thread { internal partial class RunnableImplementor : Java.Lang.Object, IRunnable { public RunnableImplementor (Action handler, bool removable = false); public void Run(); static Dictionary<Action, RunnableImplementor> instances; public static RunnableImplementor Remove(Action handler); } } which can be used in the `View` overloads (simplified for exposition): namespace Android.Views; partial class View { public bool Post(Action action) => Post(new RunnableImplementor(action, removable: true)); public bool RemoveCallbacks(Action action) => RemoveCallbacks(RunnableImplementor.Remove(action)); } This works, but there's a problem [^0]: when `removable:true` is used, then `handler` is added to `instances`, and `handler` is only removed when: 1. `RunnableImplementor.Run()` is invoked, or 2. `RunnableImplementor.Remove(Action)` is invoked. `RunnableImplementor.Remove(Action)` is only invoked if `View.RemoveAction()` is invoked. Thus, the question: is there a way to use `View.Post()` and *not* invoke `RunnableImplementor.Run()`? Turns Out™, ***Yes***: var v = new View(context); v.Post(() => …); then…do nothing with `v`. While the `View.post(Runnable)` docs state: > Causes the Runnable to be added to the message queue. > The runnable will be run on the user interface thread. that's not *quite* true. More specifically, the `Runnable`s added to the `View` via `View.post(Runnable)` are only *actually* added to the message queue *if and when* the `View` is added to a `Window` [^1]. If the `View` is never added to a `Window`, then *all the `Runnable`s are never invoked*. Which brings us to the C# leak: if we call `View.Post(Action)` and never add the `View` to a `Window`, then `RunnableImplementor.Run()` is never executed. If `RunnableImplementor.Run()` isn't executed, then the `RunnableImplementor` instance will never be removed from `RunnableImplementor.instances`, and as `instances` is a "GC root" it will keep *all of* the `RunnableImplementor` instance, the Java-side `RunnableImplementor` peer instance (via GREF), and the `Action` alive, forever. I could observe this behavior in a MAUI unit test that: 1. Creates a `ListView` 2. Creates the platform view that implements `ListView` 3. Never adds any of these objects to the `Window` 4. Makes sure none of the things leak -- *this fails* Fix this by changing `RunnableImplementor.instances` to be a `ConditionalWeakTable<Action, RunnableImplementor>`. This *prevents* `RunnableImplementor.instances` from acting as a GC root, allowing both the `Action` keys and `RunnableImplementor` values to be collected. dotnet/maui#18757 is more or less the same scenario, with one added Reproduction step that should be called out: > * Calling `View.Post(Action)` repeatedly (e.g. in a loop, on a > timer etc) will eventually cause the application to crash with > the message global reference table overflow *This particular part is not solvable*. Android has a GREF limit of ~52000 entries. If you try to create ~52000 GREFs in a way that doesn't allow the GC to collect things, then the app will crash, and there is nothing we can do about it: var v = new View(context); for (int i = 0; i < 53000; ++i) { int x = i; v.Post(() => Console.WriteLine(x)); } // Boom; attempting to add 53k Actions to a View will require // creating 53k `RunnableImplementor` instances, which will // create 53k GREFs, which will cause a Very Bad Time™. TODO: Address [^0] and dispose of the `RunnableImplementor` instance when `View.Post()` returns `false`. [0]: https://developer.android.com/reference/android/view/View#post(java.lang.Runnable) [1]: https://developer.android.com/reference/android/view/View#removeCallbacks(java.lang.Runnable) [2]: https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/view/View.java;l=19618 [3]: https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/view/View.java;l=21363 [^0]: There's at least two problems; another problem is that we leak if `View.post(Runnable)` returns *false*. This will be addressed later. [^1]: If you never add the `View` to a `Window`, then the `Runnables` just hang around until the GC decides to collect them: 1. When a `View` is *not* attached to a `Window`, then `View.mAttachInfo` is null, [so we call `getRunQueue()`][2]: public boolean post(Runnable action) { … getRunQueue().post(action); return true; } 2. `View.getRunQueue()` creates a `HandlerActionQueue`, sets `View.mRunQueue`. 3. The only place `View.mRunQueue` is "meaningfully used" is within [`View.dispatchAttachedToWindow()`][3], which "transfers" the `Runnables` within the `HandlerActionQueue` to the UI handler. 4. `View.dispatchAttachedToWindow()` is only executed when the `View` is attacked to a `Window`.
grendello
pushed a commit
to dotnet/android
that referenced
this pull request
Apr 25, 2024
Context: dotnet/maui#18757 Context: dotnet/maui#22007 Context: https://github.com/dotnet/maui/blob/440fa7f6ff5602e9cbe23249df5d13c45fb261e1/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/ListViewRenderer.cs#L484-L491 Context: xamarin/monodroid@f4ffb99 Context: 5777337 [`android.view.View.post(Runnable)`][0] adds a `Runnable` callback to the message queue, to be later run on the UI thread. Commit xamarin/monodroid@f4ffb99f, imported in 5777337, added a `View.Post(Action)` overload for this method to make it easier to use. There is also a [`View.removeCallbacks(Runnable)`][1] method which allows removing the specified callback from the message queue. A `View.RemoveCallbacks(Action)` method was also added, permitting: Action a = () => …; View v = new View(context); v.Post(a); v.RemoveCallbacks(a); To make this work, we needed a way look up the `java.lang.Runnable` that corresponds to a given `Action`. Enter `RunnableImplementor` and `RunnableImplementor.instances`: namespace Java.Lang; partial class Thread { internal partial class RunnableImplementor : Java.Lang.Object, IRunnable { public RunnableImplementor (Action handler, bool removable = false); public void Run(); static Dictionary<Action, RunnableImplementor> instances; public static RunnableImplementor Remove(Action handler); } } which can be used in the `View` overloads (simplified for exposition): namespace Android.Views; partial class View { public bool Post(Action action) => Post(new RunnableImplementor(action, removable: true)); public bool RemoveCallbacks(Action action) => RemoveCallbacks(RunnableImplementor.Remove(action)); } This works, but there's a problem [^0]: when `removable:true` is used, then `handler` is added to `instances`, and `handler` is only removed when: 1. `RunnableImplementor.Run()` is invoked, or 2. `RunnableImplementor.Remove(Action)` is invoked. `RunnableImplementor.Remove(Action)` is only invoked if `View.RemoveAction()` is invoked. Thus, the question: is there a way to use `View.Post()` and *not* invoke `RunnableImplementor.Run()`? Turns Out™, ***Yes***: var v = new View(context); v.Post(() => …); then…do nothing with `v`. While the `View.post(Runnable)` docs state: > Causes the Runnable to be added to the message queue. > The runnable will be run on the user interface thread. that's not *quite* true. More specifically, the `Runnable`s added to the `View` via `View.post(Runnable)` are only *actually* added to the message queue *if and when* the `View` is added to a `Window` [^1]. If the `View` is never added to a `Window`, then *all the `Runnable`s are never invoked*. Which brings us to the C# leak: if we call `View.Post(Action)` and never add the `View` to a `Window`, then `RunnableImplementor.Run()` is never executed. If `RunnableImplementor.Run()` isn't executed, then the `RunnableImplementor` instance will never be removed from `RunnableImplementor.instances`, and as `instances` is a "GC root" it will keep *all of* the `RunnableImplementor` instance, the Java-side `RunnableImplementor` peer instance (via GREF), and the `Action` alive, forever. I could observe this behavior in a MAUI unit test that: 1. Creates a `ListView` 2. Creates the platform view that implements `ListView` 3. Never adds any of these objects to the `Window` 4. Makes sure none of the things leak -- *this fails* Fix this by changing `RunnableImplementor.instances` to be a `ConditionalWeakTable<Action, RunnableImplementor>`. This *prevents* `RunnableImplementor.instances` from acting as a GC root, allowing both the `Action` keys and `RunnableImplementor` values to be collected. dotnet/maui#18757 is more or less the same scenario, with one added Reproduction step that should be called out: > * Calling `View.Post(Action)` repeatedly (e.g. in a loop, on a > timer etc) will eventually cause the application to crash with > the message global reference table overflow *This particular part is not solvable*. Android has a GREF limit of ~52000 entries. If you try to create ~52000 GREFs in a way that doesn't allow the GC to collect things, then the app will crash, and there is nothing we can do about it: var v = new View(context); for (int i = 0; i < 53000; ++i) { int x = i; v.Post(() => Console.WriteLine(x)); } // Boom; attempting to add 53k Actions to a View will require // creating 53k `RunnableImplementor` instances, which will // create 53k GREFs, which will cause a Very Bad Time™. TODO: Address [^0] and dispose of the `RunnableImplementor` instance when `View.Post()` returns `false`. [0]: https://developer.android.com/reference/android/view/View#post(java.lang.Runnable) [1]: https://developer.android.com/reference/android/view/View#removeCallbacks(java.lang.Runnable) [2]: https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/view/View.java;l=19618 [3]: https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/view/View.java;l=21363 [^0]: There's at least two problems; another problem is that we leak if `View.post(Runnable)` returns *false*. This will be addressed later. [^1]: If you never add the `View` to a `Window`, then the `Runnables` just hang around until the GC decides to collect them: 1. When a `View` is *not* attached to a `Window`, then `View.mAttachInfo` is null, [so we call `getRunQueue()`][2]: public boolean post(Runnable action) { … getRunQueue().post(action); return true; } 2. `View.getRunQueue()` creates a `HandlerActionQueue`, sets `View.mRunQueue`. 3. The only place `View.mRunQueue` is "meaningfully used" is within [`View.dispatchAttachedToWindow()`][3], which "transfers" the `Runnables` within the `HandlerActionQueue` to the UI handler. 4. `View.dispatchAttachedToWindow()` is only executed when the `View` is attacked to a `Window`.
jonathanpeppers
added a commit
to dotnet/android
that referenced
this pull request
Apr 29, 2024
Context: dotnet/maui#18757 Context: dotnet/maui#22007 Context: https://github.com/dotnet/maui/blob/440fa7f6ff5602e9cbe23249df5d13c45fb261e1/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/ListViewRenderer.cs#L484-L491 Context: xamarin/monodroid@f4ffb99 Context: 5777337 [`android.view.View.post(Runnable)`][0] adds a `Runnable` callback to the message queue, to be later run on the UI thread. Commit xamarin/monodroid@f4ffb99f, imported in 5777337, added a `View.Post(Action)` overload for this method to make it easier to use. There is also a [`View.removeCallbacks(Runnable)`][1] method which allows removing the specified callback from the message queue. A `View.RemoveCallbacks(Action)` method was also added, permitting: Action a = () => …; View v = new View(context); v.Post(a); v.RemoveCallbacks(a); To make this work, we needed a way look up the `java.lang.Runnable` that corresponds to a given `Action`. Enter `RunnableImplementor` and `RunnableImplementor.instances`: namespace Java.Lang; partial class Thread { internal partial class RunnableImplementor : Java.Lang.Object, IRunnable { public RunnableImplementor (Action handler, bool removable = false); public void Run(); static Dictionary<Action, RunnableImplementor> instances; public static RunnableImplementor Remove(Action handler); } } which can be used in the `View` overloads (simplified for exposition): namespace Android.Views; partial class View { public bool Post(Action action) => Post(new RunnableImplementor(action, removable: true)); public bool RemoveCallbacks(Action action) => RemoveCallbacks(RunnableImplementor.Remove(action)); } This works, but there's a problem [^0]: when `removable:true` is used, then `handler` is added to `instances`, and `handler` is only removed when: 1. `RunnableImplementor.Run()` is invoked, or 2. `RunnableImplementor.Remove(Action)` is invoked. `RunnableImplementor.Remove(Action)` is only invoked if `View.RemoveAction()` is invoked. Thus, the question: is there a way to use `View.Post()` and *not* invoke `RunnableImplementor.Run()`? Turns Out™, ***Yes***: var v = new View(context); v.Post(() => …); then…do nothing with `v`. While the `View.post(Runnable)` docs state: > Causes the Runnable to be added to the message queue. > The runnable will be run on the user interface thread. that's not *quite* true. More specifically, the `Runnable`s added to the `View` via `View.post(Runnable)` are only *actually* added to the message queue *if and when* the `View` is added to a `Window` [^1]. If the `View` is never added to a `Window`, then *all the `Runnable`s are never invoked*. Which brings us to the C# leak: if we call `View.Post(Action)` and never add the `View` to a `Window`, then `RunnableImplementor.Run()` is never executed. If `RunnableImplementor.Run()` isn't executed, then the `RunnableImplementor` instance will never be removed from `RunnableImplementor.instances`, and as `instances` is a "GC root" it will keep *all of* the `RunnableImplementor` instance, the Java-side `RunnableImplementor` peer instance (via GREF), and the `Action` alive, forever. I could observe this behavior in a MAUI unit test that: 1. Creates a `ListView` 2. Creates the platform view that implements `ListView` 3. Never adds any of these objects to the `Window` 4. Makes sure none of the things leak -- *this fails* Fix this by changing `RunnableImplementor.instances` to be a `ConditionalWeakTable<Action, RunnableImplementor>`. This *prevents* `RunnableImplementor.instances` from acting as a GC root, allowing both the `Action` keys and `RunnableImplementor` values to be collected. dotnet/maui#18757 is more or less the same scenario, with one added Reproduction step that should be called out: > * Calling `View.Post(Action)` repeatedly (e.g. in a loop, on a > timer etc) will eventually cause the application to crash with > the message global reference table overflow *This particular part is not solvable*. Android has a GREF limit of ~52000 entries. If you try to create ~52000 GREFs in a way that doesn't allow the GC to collect things, then the app will crash, and there is nothing we can do about it: var v = new View(context); for (int i = 0; i < 53000; ++i) { int x = i; v.Post(() => Console.WriteLine(x)); } // Boom; attempting to add 53k Actions to a View will require // creating 53k `RunnableImplementor` instances, which will // create 53k GREFs, which will cause a Very Bad Time™. TODO: Address [^0] and dispose of the `RunnableImplementor` instance when `View.Post()` returns `false`. [0]: https://developer.android.com/reference/android/view/View#post(java.lang.Runnable) [1]: https://developer.android.com/reference/android/view/View#removeCallbacks(java.lang.Runnable) [2]: https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/view/View.java;l=19618 [3]: https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/view/View.java;l=21363 [^0]: There's at least two problems; another problem is that we leak if `View.post(Runnable)` returns *false*. This will be addressed later. [^1]: If you never add the `View` to a `Window`, then the `Runnables` just hang around until the GC decides to collect them: 1. When a `View` is *not* attached to a `Window`, then `View.mAttachInfo` is null, [so we call `getRunQueue()`][2]: public boolean post(Runnable action) { … getRunQueue().post(action); return true; } 2. `View.getRunQueue()` creates a `HandlerActionQueue`, sets `View.mRunQueue`. 3. The only place `View.mRunQueue` is "meaningfully used" is within [`View.dispatchAttachedToWindow()`][3], which "transfers" the `Runnables` within the `HandlerActionQueue` to the UI handler. 4. `View.dispatchAttachedToWindow()` is only executed when the `View` is attacked to a `Window`.
rmarinho
added a commit
that referenced
this pull request
May 6, 2024
* [iOS] Fix crash closing Popup with WebView (#21718) * Added repro sample * Fix the issue * Added UI Test * Updated csproj * More changes * Removed sample and test * More changes * Removed unnecesary changes * Update dependencies from https://github.com/dotnet/xharness build 20240408.1 (#21839) Microsoft.DotNet.XHarness.CLI , Microsoft.DotNet.XHarness.TestRunners.Common , Microsoft.DotNet.XHarness.TestRunners.Xunit From Version 9.0.0-prerelease.24203.1 -> To Version 9.0.0-prerelease.24208.1 Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> * [Android] Fix crash navigating back (#20420) * Fix the issue * Added device test * [XC] Fix x:DataType resolution for BindingContext (#21454) * Add test * Add special case for resolving x:DataType for the binding context property * [X] Fix TargetType simplification bug (#21764) * Add unit test * Simplify TargetType=x:Type before setting resources * fix: Use AppContext.BaseDirectory instead of Environment.CurrentDirectory (#21797) #21750 --------- Co-authored-by: Eilon Lipton <Eilon@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@gmail.com> * Remove 2nd webview2 used to add `base` tag to passed html * Revert script to OG version * [ios/catalyst] fix memory leak with CollectionView (#21850) Fixes: #20710 Context: https://github.com/marco-skizza/MauiCollectionView Testing the sample above, you can see a memory leak when setting up a `CollectionView`'s `DataTemplate` in XAML, but it appears to work just fine with C#. Using `dotnet-gcdump` with a `Release` build of the app, I see: ![image](https://github.com/dotnet/maui/assets/840039/6b4b8682-2989-4108-8726-daf46da146e4) You can cause the C# version to leak, if you make the lambda an instance method: * jonathanpeppers/iOSMauiCollectionView@3e5fb02 XAML just *happens* to use an instance method, no matter if XamlC is on/off. I was able to reproduce this in `CollectionViewTests.iOS.cs` by making the `CollectionView` look like a "user control" and create an instance method. There is a "cycle" that causes the problem here: * `Microsoft.Maui.Controls.Handlers.Items.VerticalCell` (note this is a `UICollectionViewCell`) -> * `DataTemplate` -> * `Func<object>` -> * `PageXamlLeak` (or `PageCsOk` w/ my change) -> * `CollectionView` -> * `Microsoft.Maui.Controls.Handlers.Items.VerticalCell` The solution being to make `TemplatedCell` (which is a subclass of `VerticalCell`) hold only a `WeakReference` to the `DataTemplate`. With this change in place, the test passes. ~~ Notes about Catalyst ~~ 1. The new test passes on Catalyst (with the `#if` removed), if you run it by itself 2. If you run *all* the tests, it seems like there is a `Window` -> `Page` -> `CollectionView` that makes the test fail. 3. This seems like it's all related to the test setup, and not a bug. It seems like what is here might be OK for now? If Catalyst leaks, it would probably leak on iOS as well and the test passes there. * [tests] add `SourceGen.UnitTests` to CI (#21889) Context: #21725 In #21725 we noticed the source generator unit tests (for `*.xaml.g.cs`) don't run on CI. Whoops! * [C] Propagate resources when reparenting (#21879) * [C] Propagate resources when reparenting - fixes a bug when the theme is changed while the control/page isn't parented. Should fix @LeDahu22 reported issue of #21744 * move logic to resourcesextensions * [iOS] Fix crash closing Popup with WebView (#21923) * [iOS] Fix crash closing Popup with WebView (#21718) * Added repro sample * Fix the issue * Added UI Test * Updated csproj * More changes * Removed sample and test * More changes * Removed unnecesary changes * Added UITest * Update Issue21846.xaml.cs * Update Issue21846Modal.xaml.cs * Update Issue21846.cs --------- Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com> * Update locker.yml (#21894) * Bump the Locker action version Refer to microsoft/vscode-github-triage-actions#210 * Restrict the Locker action to dotnet org * do not scroll if the view is in the navbar (#21806) * [windows] less interops in calling `get_Children` (#21792) The PR trivially attempts to decrease the number of interops, using a local variable. Contributes to #21787 * [controls] fix leak in ImageSource, caused by Task that never completes (#21928) Context: https://github.com/AdamEssenmacher/MemoryToolkit.Maui/tree/main/samples Testing the above sample, I saw some odd results in a `.gcdump` related to `UriImageSource` inside a `CollectionView`: AsyncTaskMethodBuilder+AsyncStateMachineBox<VoidTaskResult, Microsoft.Maui.Controls.ImageElement+<CancelOldValue>d__10>, Count: 182 Dictionary+Entry<Int32, Task>[], Count: 182 Dictionary<Int32, Task> [Static variable Task.s_currentActiveTasks], Count: 1 It appears that we `await` a `Task` that never completes, which is: https://github.com/dotnet/maui/blob/8e4450cbc14932a6c74aeb8b7bfee9c94eca18b0/src/Controls/src/Core/ImageElement.cs#L129 The `Task` also holds onto the `ImageSource` and whatever memory it happened to use. That would be ok if the `Task` completed. I could reproduce the problem in a test: [Fact] public async Task CancelCompletes() { var imageSource = new StreamImageSource { Stream = _ => Task.FromResult<Stream>(new MemoryStream()) }; await ((IStreamImageSource)imageSource).GetStreamAsync(); await imageSource.Cancel(); // This should complete! } This non-completing `Task` can occur when changing `Image.Source`, which would certainly be bad while scrolling a `CollectionView`! To fix, I refactored the code slightly and had the problematic case return: return Task.FromResult(false); * More IndexOf() optimization (#20083) * Use better IndexOf where possible * apply similar performance improvement to the predicate-based IndexOf() * use built-in IndexOfChild on Android * add braces as per new editorconfig rule * use TryGetValue to avoid double lookup --------- Co-authored-by: Edward Miller <symbiogenisis@outlook.com> * Go back to ignoring ExpectingPageNotToBreak (#21940) * [ios/catalyst] fix memory leak in gestures (#21887) * [ios/catalyst] fix memory leak in gestures Related: #21089 Context: jonathanpeppers/iOSMauiCollectionView@547b7fa Doing something like this and running on iOS or Catalyst: <CollectionView.ItemTemplate> <DataTemplate> <Label Text="{Binding Name}"> <Label.GestureRecognizers> <TapGestureRecognizer Command="{Binding BindingContext.Command}" /> </Label.GestureRecognizers> </Label> </DataTemplate> </CollectionView.ItemTemplate> Causes a memory leak. I could reproduce this in a test that is a bit more elaborate than #21089, showing issues for different types of gestures: * Usage of `ShouldReceiveTouch` creates a cycle: * `GesturePlatformManager` -> `UIGestureRecognizer.ShouldReceiveTouch` -> `GesturePlatformManager` * We can move `ShouldReceiveTouch` to a "proxy" class, and avoid the cycle. * `PanGestureRecognizer` has closures that cause the same cycle: * `this.PlatformView` -> `eventTracker?.PlatformView` * `panRecognizer` -> `((PanGestureRecognizer)panGestureRecognizer)` * These already had a `WeakReference` to use instead, but we could just make use of them. There *may* be a general problem with `CollectionView` in how it doesn't tear down `GesturePlatformManager` in the same way for children. But in either case, the problems above are cycles that should be broken. * Fix test on Windows * Setup scaffolding for legacy test runner (#21423) * Just added a couple of tests * Focus on Android and iOS (for now) * Added legacy tests to the pipeline * More changes * More changes * Fixed build * Created ui-tests-legacy-steps.yml * More changes * More changes * Updated Appium to RC7 * Update ui-tests.yml * Fixed test on Android * Fixed test * More changes * More changes * Updated snapshot * More fixes * Clean code * Run test only on iOS * Added pending constant * Fix build error * More changes * Update ControlGallery.iOS.Appium.UITests.csproj * Update ControlGallery.Shared.Appium.UITests.csproj * Trying to run only on iOS * This is the fix, I feel it * Fix merge error --------- Co-authored-by: Gerald Versluis <gerald.versluis@microsoft.com> * [WinUI] `GesturePlatformManager.Windows.cs` - unused variable (#21953) * Code style * Remove unused line * IndexOf() stackOverflow fix (#20083) (#21965) * Update EnumerableExtensions.cs * Update EnumerableExtensions.cs * Add visual test for webview2 on windows * add EnumerableTests (#21978) Co-authored-by: Edward Miller <symbiogenisis@outlook.com> * Re-generate template localization (#21988) * Add s/triaged label for issues opened by (core) team (#22006) * Add s/triaged label for issues opened by (core) team Alternate approach to #21775 as that clearly didn't work. Hoping this _will_ work! From the original PR: > Adds a rule to the bot that adds the s/triaged label to an issue which is opened by the core team (someone with write permissions on the repo). A lot of times these issues are tasks and/or very cryptic to the triage team. This way, the triage team will skip these issues and we assume the issues are valid ones and/or not issues that need triage. * Update resourceManagement.yml * Add latest .NET9 and .NET8 (#22034) * [iOS] Add UITest for #21806 (#21951) * Add UITest and use IsDescendant of ContainverVC * Add button to focus entry in the navbar * UITests working separately * Use the modal stack * [ios/catalyst] fix memory leaks in ListView (#22007) * [ios/catalyst] fix memory leaks in ListView Fixes: #20025 I could reproduce the leak here, pretty easily with changes to `MemoryTests`. `ListView` has several cycles that prevent garbage collection from happening: * `FormsUITableViewController` has `ListView _list` * `FormsUITableViewController` -> `ListView` -> `ListViewRenderer` -> `FormsUITableViewController` * `ListViewDataSource` has `UITableView _uiTableView` * `ListViewDataSource` -> `UITableView` -> `ListViewDataSource` * `ListViewDataSource` has `FormsUITableViewController _uiTableViewController` * `ListViewDataSource` -> `FormsUITableViewController` -> `UITableView` -> `ListViewDataSource` * `ListViewDataSource` has `protected ListView List` * `ListViewDataSource` -> `ListView` -> `ListViewRenderer` -> `FormsUITableViewController` -> `UITableView` -> `ListViewDataSource` I changed the above fields to all be `WeakReference<T>` and the leaks went away. * Tentative fix for Android Context: #18757 * Revert "Tentative fix for Android" This reverts commit b294779. * Ignore ListView test on Android for now * [UI Testing] Split mouse and touch Appium actions and added pending ones (TouchAndHold etc) (#21305) * Split mouse and touch Appium actions * More changes * Fixes * Fix build errors * More changes * Fix build errors * More updates * [Windows] Use correct build version check (#22013) * [Windows] Use correct build version check Windows 11 version isn't actually 11.*, it's 10.0.21996 * Update Connectivity.uwp.cs * Navigate Directly to Test (#22019) * Navigate Directly To Test * - add additional wait for element * Fix main (#22065) Remove duplicate Tap method * [iOS] Implemented PrefersHomeIndicatorAutoHidden, PrefersStatusBarHidden and PreferredStatusBarUpdateAnimation Platform Specifics (#20069) * iOS page specifics * Added a sample (#19970) * Code refactor * Replaced public with internal --------- Co-authored-by: dustin-wojciechowski <dustin.wojciechowski@microsoft.com> * Reenabled ReturnsNonEmptyNativeBoundingBox tests on Windows (#20238) * [iOS] Allow compat iOS buttons to resize image and to respect spacing and padding (#21759) * Fix the iOS button to resize the image, respect padding, and respect spacing * allow UITest - waiting for CI results * Allow the image to actually take up the whole space and fix issue with buttons without images or titles resizing * Add UITest screenshot * remove changes to sandbox * Added manual testing sample * More changes in the sample * Do not hide the text if it doesn't fit and resize if no title * UITests cannot share the same name --------- Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com> * Fix null reference exception in KeyboardAutoManagerScroll when UIWindow is null (#21753) * Fix null reference when window is null When using certain controls ie bottom sheets and search bars there can be a crash when the keyboard displays. Check for null & return early if it is. * set flag to false if returning * remove change on line 1 * Add IsDescendant of ContainerVC * remove the extra IsKeyboardAutoScrollHandling flag * remove extra style changes * Add UITest * Use UIView AccessibilityIdentifier on UITest * Add ignore in platform and remove catch in favor of rebasing main --------- Co-authored-by: tj-devel709 <tj.devel709@gmail.com> * Fix screenshot for 18242 for new test behavior (#22081) * [android] use Java primitive `boolean` for `UriImageSource` (#22040) Context: https://cs.android.com/android/platform/superproject/main/+/main:libcore/ojluni/annotations/hiddenapi/java/lang/Boolean.java;l=109?q=boolean.java Some internal APIs in MAUI were using `java.lang.Boolean` when they could just use `boolean` instead. Java `boolean` is a primitive type, while `java.lang.Boolean` is an object (that can be null). This is similar to `bool` vs `bool?` in C#, except Java does not have `struct`. This avoids: * JNI field lookup of `Boolean.TRUE` and `Boolean.FALSE` on startup * JNI field reads of `Boolean.TRUE` and `Boolean.FALSE` on every `UriImageSource` creation * Creating a C# instance of `Java.Lang.Boolean`, and the bookkeeping for reusing C# instances I was also going to change `ImageLoaderCallback.onComplete`, but it is listed as a public API. This is a small improvement, but should help all `UriImageSource` on Android. * Fix (#22033) * fix CA1864 (#22092) Co-authored-by: Edward Miller <symbiogenisis@outlook.com> * Improve error logging for failed resizetizering (#22064) * Improve error logging for failed resizetizering * Fix tests * [UI Testing] Implement PressEnter method (#22112) * Implement PressEnter method on appium * IsKeyboardShown not implemented on Windows * Updated test * Fix TabbedPage title displaying incorrectly (#17039) * Fix TabbedPage title displaying incorrectly Presently, when a navigation page is created and a tabbed page is added to it with children, the navigation page uses the tabbed page's selected child title as its title. This behavior is unexpected when using standard tabs (tabs on the top) in a navigation page on Android. Without a custom tabbed page, this behavior makes sense, especially for bottom tabs; however, when a custom tabbed page is defined, it seems that the title should not be overridden. Fixes #8577 * Add device tests * Add unit tests * Updated test * More tests * Fixed test not being able to fail * Changed GetTitle to work with more than just TabbedPages * Removed tests that deal with Child page titles replacing the Toolbar title --------- Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com> Co-authored-by: Dustin Wojciechowski <dustin.wojciechowski@microsoft.com> Co-authored-by: Dustin Wojciechowski <dustinwo@microsoft.com> * Update dependencies from https://github.com/dotnet/xharness build 20240424.1 (#22119) Microsoft.DotNet.XHarness.CLI , Microsoft.DotNet.XHarness.TestRunners.Common , Microsoft.DotNet.XHarness.TestRunners.Xunit From Version 9.0.0-prerelease.24208.1 -> To Version 9.0.0-prerelease.24224.1 Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> * [Android] Crash removing item from CarouselView - fix (#22107) * Fix #19786 * Added a Ui test (#19786) * Update MauiCarouselRecyclerView.cs * [iOS] Shell page title fix (#20575) * [iOS] Shell page title fix (#20199) * Added a UiTest (#20199) * Added pending snapshot * Shell page title fix --------- Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com> * Add VS Code Extension to move-to-vs-feedback label (#22152) * Android mipmap/appicon failing with "APT2260" (#21838) Context https://developercommunity.visualstudio.com/t/Android-mipmapappicon-failing-with-APT/10622494 Users are reporting this error quite allot. During a build it fails with the following error ``` APT2260 resource mipmap/appicon (aka xxx:mipmap/appicon) not found. ``` Further investigation this only appears to happen during a full build of all the platforms. Specifying `-f net8.0-android` on the build this does not seem to happen at all. The problem is the build gets into a weird situation where the `mauiimage.stamp` file is present but none of the generated images are. Considering that the stamp file gets generated AFTER the images its kinda difficult to see how this even occurs. The work around for this is to write the list of generated image files to `mauiimage.outputs`. We can then read this list back on subsequent builds and use that list for the `Outputs` of the `ResizetizeImages` target. This means the target will run if either the stamp file or any of the expected images are not present. * Create similarIssues.yml (#22170) * Support manual triggering of similar issues (#22176) --------- Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com> Co-authored-by: dotnet-maestro[bot] <42748379+dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: Šimon Rozsíval <simon@rozsival.com> Co-authored-by: Precious Nyasulu <preciousnyasulu441@gmail.com> Co-authored-by: Eilon Lipton <Eilon@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@gmail.com> Co-authored-by: Mike Corsaro <mikecorsaro@microsoft.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Stephane Delcroix <stephane@delcroix.org> Co-authored-by: Shane Neuville <shneuvil@microsoft.com> Co-authored-by: Igor Velikorossov <RussKie@users.noreply.github.com> Co-authored-by: TJ Lambert <50846373+tj-devel709@users.noreply.github.com> Co-authored-by: MartyIX <203266+MartyIX@users.noreply.github.com> Co-authored-by: Edward Miller <symbiogenesis@outlook.com> Co-authored-by: Edward Miller <symbiogenisis@outlook.com> Co-authored-by: Gerald Versluis <gerald.versluis@microsoft.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com> Co-authored-by: Matthew Leibowitz <mattleibow@live.com> Co-authored-by: Mike Corsaro <corsarom@gmail.com> Co-authored-by: dustin-wojciechowski <dustin.wojciechowski@microsoft.com> Co-authored-by: Axemasta <33064621+Axemasta@users.noreply.github.com> Co-authored-by: tj-devel709 <tj.devel709@gmail.com> Co-authored-by: Adam Anderson <andersad@gmail.com> Co-authored-by: Dustin Wojciechowski <dustinwo@microsoft.com> Co-authored-by: Dean Ellis <dellis1972@googlemail.com> Co-authored-by: Craig Loewen <crloewen@microsoft.com>
jonathanpeppers
added a commit
to dotnet/android
that referenced
this pull request
May 6, 2024
Context: dotnet/maui#18757 Context: dotnet/maui#22007 Context: https://github.com/dotnet/maui/blob/440fa7f6ff5602e9cbe23249df5d13c45fb261e1/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/ListViewRenderer.cs#L484-L491 Context: xamarin/monodroid@f4ffb99 Context: 5777337 [`android.view.View.post(Runnable)`][0] adds a `Runnable` callback to the message queue, to be later run on the UI thread. Commit xamarin/monodroid@f4ffb99f, imported in 5777337, added a `View.Post(Action)` overload for this method to make it easier to use. There is also a [`View.removeCallbacks(Runnable)`][1] method which allows removing the specified callback from the message queue. A `View.RemoveCallbacks(Action)` method was also added, permitting: Action a = () => …; View v = new View(context); v.Post(a); v.RemoveCallbacks(a); To make this work, we needed a way look up the `java.lang.Runnable` that corresponds to a given `Action`. Enter `RunnableImplementor` and `RunnableImplementor.instances`: namespace Java.Lang; partial class Thread { internal partial class RunnableImplementor : Java.Lang.Object, IRunnable { public RunnableImplementor (Action handler, bool removable = false); public void Run(); static Dictionary<Action, RunnableImplementor> instances; public static RunnableImplementor Remove(Action handler); } } which can be used in the `View` overloads (simplified for exposition): namespace Android.Views; partial class View { public bool Post(Action action) => Post(new RunnableImplementor(action, removable: true)); public bool RemoveCallbacks(Action action) => RemoveCallbacks(RunnableImplementor.Remove(action)); } This works, but there's a problem [^0]: when `removable:true` is used, then `handler` is added to `instances`, and `handler` is only removed when: 1. `RunnableImplementor.Run()` is invoked, or 2. `RunnableImplementor.Remove(Action)` is invoked. `RunnableImplementor.Remove(Action)` is only invoked if `View.RemoveAction()` is invoked. Thus, the question: is there a way to use `View.Post()` and *not* invoke `RunnableImplementor.Run()`? Turns Out™, ***Yes***: var v = new View(context); v.Post(() => …); then…do nothing with `v`. While the `View.post(Runnable)` docs state: > Causes the Runnable to be added to the message queue. > The runnable will be run on the user interface thread. that's not *quite* true. More specifically, the `Runnable`s added to the `View` via `View.post(Runnable)` are only *actually* added to the message queue *if and when* the `View` is added to a `Window` [^1]. If the `View` is never added to a `Window`, then *all the `Runnable`s are never invoked*. Which brings us to the C# leak: if we call `View.Post(Action)` and never add the `View` to a `Window`, then `RunnableImplementor.Run()` is never executed. If `RunnableImplementor.Run()` isn't executed, then the `RunnableImplementor` instance will never be removed from `RunnableImplementor.instances`, and as `instances` is a "GC root" it will keep *all of* the `RunnableImplementor` instance, the Java-side `RunnableImplementor` peer instance (via GREF), and the `Action` alive, forever. I could observe this behavior in a MAUI unit test that: 1. Creates a `ListView` 2. Creates the platform view that implements `ListView` 3. Never adds any of these objects to the `Window` 4. Makes sure none of the things leak -- *this fails* Fix this by changing `RunnableImplementor.instances` to be a `ConditionalWeakTable<Action, RunnableImplementor>`. This *prevents* `RunnableImplementor.instances` from acting as a GC root, allowing both the `Action` keys and `RunnableImplementor` values to be collected. dotnet/maui#18757 is more or less the same scenario, with one added Reproduction step that should be called out: > * Calling `View.Post(Action)` repeatedly (e.g. in a loop, on a > timer etc) will eventually cause the application to crash with > the message global reference table overflow *This particular part is not solvable*. Android has a GREF limit of ~52000 entries. If you try to create ~52000 GREFs in a way that doesn't allow the GC to collect things, then the app will crash, and there is nothing we can do about it: var v = new View(context); for (int i = 0; i < 53000; ++i) { int x = i; v.Post(() => Console.WriteLine(x)); } // Boom; attempting to add 53k Actions to a View will require // creating 53k `RunnableImplementor` instances, which will // create 53k GREFs, which will cause a Very Bad Time™. TODO: Address [^0] and dispose of the `RunnableImplementor` instance when `View.Post()` returns `false`. [0]: https://developer.android.com/reference/android/view/View#post(java.lang.Runnable) [1]: https://developer.android.com/reference/android/view/View#removeCallbacks(java.lang.Runnable) [2]: https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/view/View.java;l=19618 [3]: https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/view/View.java;l=21363 [^0]: There's at least two problems; another problem is that we leak if `View.post(Runnable)` returns *false*. This will be addressed later. [^1]: If you never add the `View` to a `Window`, then the `Runnables` just hang around until the GC decides to collect them: 1. When a `View` is *not* attached to a `Window`, then `View.mAttachInfo` is null, [so we call `getRunQueue()`][2]: public boolean post(Runnable action) { … getRunQueue().post(action); return true; } 2. `View.getRunQueue()` creates a `HandlerActionQueue`, sets `View.mRunQueue`. 3. The only place `View.mRunQueue` is "meaningfully used" is within [`View.dispatchAttachedToWindow()`][3], which "transfers" the `Runnables` within the `HandlerActionQueue` to the UI handler. 4. `View.dispatchAttachedToWindow()` is only executed when the `View` is attacked to a `Window`.
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Labels
fixed-in-8.0.40
fixed-in-9.0.0-preview.5.24307.10
memory-leak 💦
Memory usage grows / objects live forever (sub: perf)
platform/iOS 🍎
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes: #20025
I could reproduce the leak here, pretty easily with changes to
MemoryTests
.ListView
has several cycles that prevent garbage collection from happening:FormsUITableViewController
hasListView _list
FormsUITableViewController
->ListView
->ListViewRenderer
->FormsUITableViewController
ListViewDataSource
hasUITableView _uiTableView
ListViewDataSource
->UITableView
->ListViewDataSource
ListViewDataSource
hasFormsUITableViewController _uiTableViewController
ListViewDataSource
->FormsUITableViewController
->UITableView
->ListViewDataSource
ListViewDataSource
hasprotected ListView List
ListViewDataSource
->ListView
->ListViewRenderer
->FormsUITableViewController
->UITableView
->ListViewDataSource
I changed the above fields to all be
WeakReference<T>
and the leaks went away.