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

[ios] fix memory leak in TimePicker #16265

Merged
merged 3 commits into from Aug 7, 2023

Conversation

jonathanpeppers
Copy link
Member

After working on some tooling to proactively find memory leaks, I found a few in the TimePicker. This kind of highlights the pattern:

  • MauiTimePicker is a subclass of UIDatePicker

  • TimePickerHandler has a strong reference to MauiTimePicker as the PlatformView.

  • TimePickerHandler subscribes to various events and has a strong reference to MauiTimePicker: creating a cycle.

This effectively causes entire pages to leak, because TimePicker.Parent.Parent, etc. will hold a strong reference up to the entire Page.

It is not enough to unsubscribe from these events in DisconnectHandler, because it is not called in many cases.

For example, take the test:

    await InvokeOnMainThreadAsync(() =>
    {
        var layout = new Grid();
        var picker = new TimePicker();
        layout.Add(picker);
        var handler = CreateHandler<LayoutHandler>(layout);
        viewReference = new WeakReference(handler);
        handlerReference = new WeakReference(picker.Handler);
        platformViewReference = new WeakReference(picker.Handler.PlatformView);
    });

    await AssertionExtensions.WaitForGC(viewReference, handlerReference, platformViewReference);
    Assert.False(viewReference.IsAlive, "TimePicker should not be alive!");
    Assert.False(handlerReference.IsAlive, "Handler should not be alive!");
    Assert.False(platformViewReference.IsAlive, "PlatformView should not be alive!");

In this case, Grid's LayoutHandler.DisconnectHandler is called, but not the TimePickerHandler.

To solve this problem, I moved 5 events to the MauiTimePicker class and was able to remove one.

I also found a similar problem with MauiDoneAccessoryView and converted various Action callbacks to use WeakReference instead.

The test did not pass until solving all of the above.

@jonathanpeppers jonathanpeppers added the memory-leak 💦 Memory usage grows / objects live forever label Jul 20, 2023
jonathanpeppers added a commit to jonathanpeppers/maui that referenced this pull request Jul 21, 2023
This is an alternate idea to:

dotnet#16265
@jonathanpeppers
Copy link
Member Author

We are discussing if there could be a handler lifecycle change to make this easier.

For now, I might rework this change to be smaller and only fix MauiDoneAccessoryView .

Comment on lines +42 to 43
[InlineData(typeof(TimePicker))]
public async Task HandlerDoesNotLeak(Type type)
Copy link
Member Author

Choose a reason for hiding this comment

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

I reworked this PR to use the new test pattern in 0e27ca4.

The test was failing without these changes, so it makes these PRs easier.

Comment on lines 7 to +9
public partial class TimePickerHandler : ViewHandler<ITimePicker, UIDatePicker>
{
readonly UIDatePickerProxy _proxy = new();
Copy link
Member Author

Choose a reason for hiding this comment

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

I found MacCatalyst has a different handler than iOS, so I found I had to make fixes in here as well.

@jonathanpeppers jonathanpeppers marked this pull request as ready for review August 1, 2023 18:41
@Eilon Eilon added the area/perf 🏎️ Startup / Runtime performance label Aug 1, 2023
After working on some tooling to proactively find memory leaks, I found
a few in the `TimePicker`. This kind of highlights the pattern:

* `MauiTimePicker` is a subclass of `UIDatePicker`

* `TimePickerHandler` has a strong reference to `MauiTimePicker`
  as the `PlatformView`.

* `TimePickerHandler` subscribes to various events and has a strong
  reference to `MauiTimePicker`: creating a cycle.

This effectively causes entire pages to leak, because
`TimePicker.Parent.Parent`, etc. will hold a strong reference up to the
entire `Page`.

It is not enough to unsubscribe from these events in `DisconnectHandler`,
because it is not called in many cases.

For example, take the test:

    await InvokeOnMainThreadAsync(() =>
    {
        var layout = new Grid();
        var picker = new TimePicker();
        layout.Add(picker);
        var handler = CreateHandler<LayoutHandler>(layout);
        viewReference = new WeakReference(handler);
        handlerReference = new WeakReference(picker.Handler);
        platformViewReference = new WeakReference(picker.Handler.PlatformView);
    });

    await AssertionExtensions.WaitForGC(viewReference, handlerReference, platformViewReference);
    Assert.False(viewReference.IsAlive, "TimePicker should not be alive!");
    Assert.False(handlerReference.IsAlive, "Handler should not be alive!");
    Assert.False(platformViewReference.IsAlive, "PlatformView should not be alive!");

In this case, `Grid`'s `LayoutHandler.DisconnectHandler` is called, but
not the `TimePickerHandler`.

To solve this problem, I moved 5 events to the `MauiTimePicker` class
and was able to remove one.

I also found a similar problem with `MauiDoneAccessoryView` and converted
various `Action` callbacks to use `WeakReference` instead.

The test did not pass until solving all of the above.
@jonathanpeppers jonathanpeppers merged commit af27792 into dotnet:main Aug 7, 2023
39 checks passed
@jonathanpeppers jonathanpeppers deleted the TimePickerOhNo branch August 7, 2023 13:48
@github-actions github-actions bot locked and limited conversation to collaborators Dec 8, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area/perf 🏎️ Startup / Runtime performance memory-leak 💦 Memory usage grows / objects live forever platform/iOS 🍎
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants