-
Notifications
You must be signed in to change notification settings - Fork 2k
[leak-fix] Fix Picker.ItemsSource memory leak (Fixes #36272) #36274
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
Changes from 12 commits
5780cb9
6d47472
f5bcce4
2e9df5c
9daabe1
716ded3
0bc6118
3184ace
51d7901
84d0af3
72c33af
e646d4f
ae6f610
983af21
ec7e4ed
31e2614
dc3c354
3a5164b
a3db7c2
d1ffb5e
1440d3b
0579dc7
562de41
6030d6e
189ea14
b6ca332
99e93dd
3979ece
245ffc8
91554e0
ac7a4c5
e0f5482
bd887d7
16c5389
d7a699a
f321024
d457c41
57a739f
9eaacbc
eb17cfa
7effad3
eff3891
d9320e9
4216d9a
ff9480f
c6f4509
a46cf93
a28a7e3
03f0dc0
40f8a89
a6e6583
fd26832
a76a0b7
4a6713b
d18e8e2
1d9491f
bbf45f6
5de2bcd
73aaae7
217b42f
f76284d
d608f9b
28d9957
444e4c5
db69d65
5ba0f40
986db3f
5ebdf10
6f6b6ea
23cd737
80dee3d
7e6fea5
cfdc79e
92eef48
fd33a4c
69f0c0f
b8a2961
dbe8f09
c0e8bbd
6c08867
834e8e9
d287bd3
819ab16
5d75435
9644c01
245239b
b374f15
94684f1
16081ef
7df382b
137d787
e41f79f
5808fce
fd1a303
6ff6463
9a9a34a
8dca2b6
af72a03
9bf18b4
e056571
8e2706f
67109d0
deb21c1
257ac62
a27b8e2
f1c2689
16dcd07
869e703
c406eae
5b7e74c
b82e975
c8c7f68
4266f81
d7b652b
4484e16
330d9c1
9506a93
aec5dd6
c8e886e
15034ed
40e674b
cee52c3
f9f3c80
f594a31
e243d63
86a94bd
af53a67
d029551
c34e3d7
ec12d29
3a60ecd
4c61482
dbfa23a
4f82610
09a30f9
abc5b4b
14ee049
d31bd4e
50b4593
a78a96b
4807d9e
adfdf6c
7f07754
d7fa271
6ce9a00
4e7af45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,9 @@ | |
| using System.ComponentModel; | ||
| using System.Globalization; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Maui.Graphics; | ||
| using NSubstitute; | ||
| using Xunit; | ||
|
|
@@ -1040,5 +1043,134 @@ public void PickerPreservesSelectedItemAfterInsertingItemBeforeSelection() | |
| Assert.Equal("Dog", picker.SelectedItem); | ||
| Assert.Equal(2, picker.SelectedIndex); | ||
| } | ||
|
|
||
| [Fact, Category(TestCategory.Memory)] | ||
| public async Task PickerItemsSourceDoesNotLeak() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[minor] Regression Prevention and Test Coverage — None of the new regression tests added for this fix (this method through ~line 1340) reference the originating issue in a |
||
| { | ||
| // A long-lived / shared collection (for example a ViewModel-owned ObservableCollection) | ||
| // must not keep every Picker it was assigned to alive. | ||
| var sharedItemsSource = new ObservableCollection<string> { "a", "b", "c" }; | ||
|
|
||
| var references = CreatePickerReferences(sharedItemsSource); | ||
|
|
||
| Assert.False(await references.Picker.WaitForCollect(), "Picker should not be alive!"); | ||
| Assert.False(await references.Subscription.WaitForCollect(), "ItemsSource subscription should not be alive!"); | ||
| Assert.False(await references.Proxy.WaitForCollect(), "WeakNotifyCollectionChangedProxy should not be alive!"); | ||
| GC.KeepAlive(sharedItemsSource); | ||
| } | ||
|
|
||
| [Fact, Category(TestCategory.Memory)] | ||
| public async Task PickerItemsSourceChangesStillApplyAfterGc() | ||
| { | ||
| var itemsSource = new ObservableCollection<string> { "a", "b", "c" }; | ||
| var picker = new Picker { ItemsSource = itemsSource }; | ||
|
|
||
| await TestHelpers.Collect(); | ||
|
|
||
| itemsSource.Add("d"); | ||
|
|
||
| Assert.Equal(4, picker.Items.Count); | ||
| Assert.Equal("d", picker.Items[3]); | ||
| GC.KeepAlive(picker); | ||
| } | ||
|
|
||
| [Fact, Category(TestCategory.Memory)] | ||
| public async Task PickerItemsSourceClearReleasesCollectionChangedSubscription() | ||
| { | ||
| var sharedItemsSource = new ObservableCollection<string> { "a", "b", "c" }; | ||
| var picker = new Picker { ItemsSource = sharedItemsSource }; | ||
| var references = GetItemsSourceSubscriptionReferences(picker); | ||
|
|
||
| picker.ItemsSource = null; | ||
|
|
||
| Assert.False(await references.Subscription.WaitForCollect(), "ItemsSource subscription should not be alive!"); | ||
| Assert.False(await references.Proxy.WaitForCollect(), "WeakNotifyCollectionChangedProxy should not be alive!"); | ||
| GC.KeepAlive(picker); | ||
| GC.KeepAlive(sharedItemsSource); | ||
| } | ||
|
kubaflo marked this conversation as resolved.
|
||
|
|
||
| [Fact, Category(TestCategory.Memory)] | ||
| public async Task PickerItemsSourceReassignmentMovesCollectionChangedSubscription() | ||
| { | ||
| var replacementItemsSource = new ObservableCollection<string> { "replacement" }; | ||
| var picker = new Picker(); | ||
| var oldItemsSourceReference = AssignAndReplaceItemsSource(picker, replacementItemsSource); | ||
|
|
||
| replacementItemsSource.Add("added"); | ||
|
|
||
| Assert.Equal(2, picker.Items.Count); | ||
| Assert.Equal("added", picker.Items[1]); | ||
| Assert.False(await oldItemsSourceReference.WaitForCollect(), "Old ItemsSource should not be alive!"); | ||
|
|
||
| replacementItemsSource.Add("added after GC"); | ||
|
|
||
| Assert.Equal(3, picker.Items.Count); | ||
| Assert.Equal("added after GC", picker.Items[2]); | ||
| GC.KeepAlive(picker); | ||
| GC.KeepAlive(replacementItemsSource); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void PickerItemsSourceReassignmentIgnoresOldCollectionChanges() | ||
| { | ||
| var oldItemsSource = new ObservableCollection<string> { "old" }; | ||
| var replacementItemsSource = new ObservableCollection<string> { "replacement" }; | ||
| var picker = new Picker { ItemsSource = oldItemsSource }; | ||
|
|
||
| picker.ItemsSource = replacementItemsSource; | ||
| oldItemsSource.Add("ignored"); | ||
|
|
||
| Assert.Single(picker.Items); | ||
| Assert.Equal("replacement", picker.Items[0]); | ||
|
|
||
| replacementItemsSource.Add("added"); | ||
|
|
||
| Assert.Equal(2, picker.Items.Count); | ||
| Assert.Equal("added", picker.Items[1]); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void PickerItemsSourceCollectionChangedHandlerIsLazy() | ||
| { | ||
| const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance; | ||
| var handlerField = typeof(Picker).GetField("_collectionChangedEventHandler", flags); | ||
|
kubaflo marked this conversation as resolved.
|
||
| Assert.NotNull(handlerField); | ||
| var picker = new Picker { ItemsSource = new List<string> { "item" } }; | ||
|
|
||
| Assert.Null(handlerField.GetValue(picker)); | ||
|
|
||
| picker.ItemsSource = new ObservableCollection<string> { "observable item" }; | ||
|
|
||
| Assert.NotNull(handlerField.GetValue(picker)); | ||
| } | ||
|
|
||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| static (WeakReference Picker, WeakReference Subscription, WeakReference Proxy) CreatePickerReferences( | ||
| ObservableCollection<string> itemsSource) | ||
| { | ||
| var picker = new Picker { ItemsSource = itemsSource }; | ||
| var (subscription, proxy) = GetItemsSourceSubscriptionReferences(picker); | ||
| return (new WeakReference(picker), subscription, proxy); | ||
| } | ||
|
|
||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| static WeakReference AssignAndReplaceItemsSource(Picker picker, ObservableCollection<string> replacementItemsSource) | ||
| { | ||
| var oldItemsSource = new ObservableCollection<string> { "old" }; | ||
| picker.ItemsSource = oldItemsSource; | ||
| picker.ItemsSource = replacementItemsSource; | ||
| return new WeakReference(oldItemsSource); | ||
| } | ||
|
|
||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| static (WeakReference Subscription, WeakReference Proxy) GetItemsSourceSubscriptionReferences(Picker picker) | ||
|
kubaflo marked this conversation as resolved.
|
||
| { | ||
| const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance; | ||
| var subscription = typeof(Picker).GetField("_itemsSourceCollectionChangedSubscription", flags)?.GetValue(picker); | ||
| Assert.NotNull(subscription); | ||
| var proxy = subscription.GetType().GetField("_proxy", flags)?.GetValue(subscription); | ||
|
kubaflo marked this conversation as resolved.
Outdated
|
||
| Assert.NotNull(proxy); | ||
| return (new WeakReference(subscription), new WeakReference(proxy)); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.