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 in Window #13400

Merged
merged 1 commit into from
Mar 1, 2023

Conversation

jonathanpeppers
Copy link
Member

@jonathanpeppers jonathanpeppers commented Feb 16, 2023

Fixes #10029

I could reproduce a memory leak by doing:

App.Current.OpenWindow(new Window { Page = new ContentPage() });

I found that App.Current._requestedWindows just held onto every Window object forever.

Note that we can't use ConditionalWeakTable here:

https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2

After initially trying this, I added a test showing not to use it. The problem is if the string is GC'd the Window will be lost.

We can use Dictionary<string, WeakReference<Window>> instead.

The only other change I made was to use Guid.ToString("n") as it removes {, }, and - characters from the string.

After these changes, I still found Window objects hanging around that appeared to be held onto by many other objects.

Then I reviewed:

void IWindow.Destroying()
{
    SendWindowDisppearing();
    Destroying?.Invoke(this, EventArgs.Empty);
    OnDestroying();

    Application?.RemoveWindow(this);
    // This wasn't here!
    Handler?.DisconnectHandler();
}

It appears that upon Window's closing, we didn't have any code that "disconnected" the MAUI handler. After this change, I could see Window objects properly go away.

I added a unit test as well.

@jsuarezruiz jsuarezruiz added the legacy-area-perf Startup / Runtime performance label Feb 16, 2023
@rmarinho
Copy link
Member

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@jonathanpeppers jonathanpeppers force-pushed the WindowMemoryLeaks branch 3 times, most recently from 2ac2129 to 3e7e522 Compare February 23, 2023 14:40
@jonathanpeppers jonathanpeppers changed the title [controls] fix memory leak in Application.OpenWindow() [controls] fix memory leak in Window Feb 23, 2023
@jonathanpeppers jonathanpeppers marked this pull request as ready for review February 23, 2023 14:41
@jonathanpeppers
Copy link
Member Author

This might cause a test failure on iOS:

image

@jonathanpeppers
Copy link
Member Author

Can repro the test failure locally, I can say that I am a bit confused:

shot

There might be an issue in this test, will look into it.

@jonathanpeppers
Copy link
Member Author

Ok the test uses async void to do work:

await CreateHandlerAndAddToWindow<WindowHandlerStub>(window, async (handler) =>

xunit is moving on as if the test passed, but test code is continuing to run.

Will review if we have a better way to do this.

@jonathanpeppers
Copy link
Member Author

Will come back to this after this one lands:

#13536

@@ -30,7 +31,10 @@ IWindow IApplication.CreateWindow(IActivationState? activationState)
if (activationState?.State?.TryGetValue(MauiWindowIdKey, out var requestedWindowId) ?? false)
{
if (requestedWindowId != null && _requestedWindows.TryGetValue(requestedWindowId, out var w))
Copy link
Member

Choose a reason for hiding this comment

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

I'm testing this on Android and the TryGetValue here seems like it's always evaluating to false.

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 added a test for this issue, and changed it to a Dictionary<string, WeakReference<Window>> which won't cause this behavior.

I think you were getting a new string instance and ConditionalWeakTable doesn't work in that case.

// A "cloned" key should still work
string key;
{
var originalKey = table.Keys.OfType<string>().Single();
key = new string(originalKey);
Assert.NotSame(originalKey, key);
}

Fixes: dotnet#10029

I could reproduce a memory leak by doing:

    App.Current.OpenWindow(new Window { Page = new ContentPage() });

I found that `App.Current._requestedWindows` just held onto every
`Window` object forever.

Note that we *can't* use `ConditionalWeakTable` here:

https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2

After initially trying this, I added a test showing not to use it. The
problem is if the `string` is GC'd the `Window` will be lost.

We can use `Dictionary<string, WeakReference<Window>>` instead.

The only other change I made was to use `Guid.ToString("n")` as it
removes `{`, `}`, and `-` characters from the string.

After these changes, I still found `Window` objects hanging around
that appeared to be held onto by many other objects.

Then I reviewed:

    void IWindow.Destroying()
    {
        SendWindowDisppearing();
        Destroying?.Invoke(this, EventArgs.Empty);
        OnDestroying();

        Application?.RemoveWindow(this);
        // This wasn't here!
        Handler?.DisconnectHandler();
    }

It appears that upon `Window`'s closing, we didn't have any code that
"disconnected" the MAUI handler. After this change, I could see
`Window` objects properly go away.

I added a unit test as well.
@PureWeen PureWeen enabled auto-merge March 1, 2023 22:41
@PureWeen PureWeen merged commit db7f68f into dotnet:main Mar 1, 2023
@jonathanpeppers jonathanpeppers deleted the WindowMemoryLeaks branch March 2, 2023 03:27
@jonathanpeppers jonathanpeppers added the backport/suggested The PR author or issue review has suggested that the change should be backported. label Mar 2, 2023
@PureWeen
Copy link
Member

PureWeen commented Mar 2, 2023

/backport to net7.0

@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2023

Started backporting to net7.0: https://github.com/dotnet/maui/actions/runs/4315741361

@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2023

@PureWeen backporting to net7.0 failed, the patch most likely resulted in conflicts:

$ git am --3way --ignore-whitespace --keep-non-patch changes.patch

Applying: [controls] fix memory leak in `Window`
Using index info to reconstruct a base tree...
M	src/Controls/src/Core/HandlerImpl/Application/Application.Impl.cs
M	src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs
M	src/Controls/tests/Core.UnitTests/WindowsTests.cs
Falling back to patching base and 3-way merge...
Auto-merging src/Controls/tests/Core.UnitTests/WindowsTests.cs
CONFLICT (content): Merge conflict in src/Controls/tests/Core.UnitTests/WindowsTests.cs
Auto-merging src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs
Auto-merging src/Controls/src/Core/HandlerImpl/Application/Application.Impl.cs
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
Patch failed at 0001 [controls] fix memory leak in `Window`
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".
Error: The process '/usr/bin/git' failed with exit code 128

Please backport manually!

@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2023

@PureWeen an error occurred while backporting to net7.0, please check the run log for details!

Error: git am failed, most likely due to a merge conflict.

@PureWeen PureWeen added the backport/approved After some discussion or review, this PR or change was approved to be backported. label Mar 2, 2023
PureWeen added a commit that referenced this pull request Mar 2, 2023
Fixes: #10029

I could reproduce a memory leak by doing:

    App.Current.OpenWindow(new Window { Page = new ContentPage() });

I found that `App.Current._requestedWindows` just held onto every
`Window` object forever.

Note that we *can't* use `ConditionalWeakTable` here:

https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2

After initially trying this, I added a test showing not to use it. The
problem is if the `string` is GC'd the `Window` will be lost.

We can use `Dictionary<string, WeakReference<Window>>` instead.

The only other change I made was to use `Guid.ToString("n")` as it
removes `{`, `}`, and `-` characters from the string.

After these changes, I still found `Window` objects hanging around that
appeared to be held onto by many other objects.

Then I reviewed:

```csharp
void IWindow.Destroying()
{
    SendWindowDisppearing();
    Destroying?.Invoke(this, EventArgs.Empty);
    OnDestroying();

    Application?.RemoveWindow(this);
    // This wasn't here!
    Handler?.DisconnectHandler();
}
```

It appears that upon `Window`'s closing, we didn't have any code that
"disconnected" the MAUI handler. After this change, I could see `Window`
objects properly go away.

I added a unit test as well.
# Conflicts:
#	src/Controls/tests/Core.UnitTests/WindowsTests.cs
rmarinho pushed a commit that referenced this pull request Mar 3, 2023
Fixes: #10029

I could reproduce a memory leak by doing:

    App.Current.OpenWindow(new Window { Page = new ContentPage() });

I found that `App.Current._requestedWindows` just held onto every
`Window` object forever.

Note that we *can't* use `ConditionalWeakTable` here:

https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2

After initially trying this, I added a test showing not to use it. The
problem is if the `string` is GC'd the `Window` will be lost.

We can use `Dictionary<string, WeakReference<Window>>` instead.

The only other change I made was to use `Guid.ToString("n")` as it
removes `{`, `}`, and `-` characters from the string.

After these changes, I still found `Window` objects hanging around that
appeared to be held onto by many other objects.

Then I reviewed:

```csharp
void IWindow.Destroying()
{
    SendWindowDisppearing();
    Destroying?.Invoke(this, EventArgs.Empty);
    OnDestroying();

    Application?.RemoveWindow(this);
    // This wasn't here!
    Handler?.DisconnectHandler();
}
```

It appears that upon `Window`'s closing, we didn't have any code that
"disconnected" the MAUI handler. After this change, I could see `Window`
objects properly go away.

I added a unit test as well.
# Conflicts:
#	src/Controls/tests/Core.UnitTests/WindowsTests.cs
github-actions bot pushed a commit that referenced this pull request Mar 20, 2023
Fixes: #10029

I could reproduce a memory leak by doing:

    App.Current.OpenWindow(new Window { Page = new ContentPage() });

I found that `App.Current._requestedWindows` just held onto every
`Window` object forever.

Note that we *can't* use `ConditionalWeakTable` here:

https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2

After initially trying this, I added a test showing not to use it. The
problem is if the `string` is GC'd the `Window` will be lost.

We can use `Dictionary<string, WeakReference<Window>>` instead.

The only other change I made was to use `Guid.ToString("n")` as it
removes `{`, `}`, and `-` characters from the string.

After these changes, I still found `Window` objects hanging around that
appeared to be held onto by many other objects.

Then I reviewed:

```csharp
void IWindow.Destroying()
{
    SendWindowDisppearing();
    Destroying?.Invoke(this, EventArgs.Empty);
    OnDestroying();

    Application?.RemoveWindow(this);
    // This wasn't here!
    Handler?.DisconnectHandler();
}
```

It appears that upon `Window`'s closing, we didn't have any code that
"disconnected" the MAUI handler. After this change, I could see `Window`
objects properly go away.

I added a unit test as well.
# Conflicts:
#	src/Controls/tests/Core.UnitTests/WindowsTests.cs
rmarinho pushed a commit that referenced this pull request Mar 21, 2023
Fixes: #10029

I could reproduce a memory leak by doing:

    App.Current.OpenWindow(new Window { Page = new ContentPage() });

I found that `App.Current._requestedWindows` just held onto every
`Window` object forever.

Note that we *can't* use `ConditionalWeakTable` here:

https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2

After initially trying this, I added a test showing not to use it. The
problem is if the `string` is GC'd the `Window` will be lost.

We can use `Dictionary<string, WeakReference<Window>>` instead.

The only other change I made was to use `Guid.ToString("n")` as it
removes `{`, `}`, and `-` characters from the string.

After these changes, I still found `Window` objects hanging around that
appeared to be held onto by many other objects.

Then I reviewed:

```csharp
void IWindow.Destroying()
{
    SendWindowDisppearing();
    Destroying?.Invoke(this, EventArgs.Empty);
    OnDestroying();

    Application?.RemoveWindow(this);
    // This wasn't here!
    Handler?.DisconnectHandler();
}
```

It appears that upon `Window`'s closing, we didn't have any code that
"disconnected" the MAUI handler. After this change, I could see `Window`
objects properly go away.

I added a unit test as well.
# Conflicts:
#	src/Controls/tests/Core.UnitTests/WindowsTests.cs

Co-authored-by: Shane Neuville <shneuvil@microsoft.com>
@jonathanpeppers jonathanpeppers added memory-leak 💦 Memory usage grows / objects live forever (sub: perf) and removed legacy-area-perf Startup / Runtime performance labels Jul 12, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Dec 13, 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 Aug 2, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
backport/approved After some discussion or review, this PR or change was approved to be backported. backport/suggested The PR author or issue review has suggested that the change should be backported. fixed-in-8.0.0-preview.3.8149 Look for this fix in 8.0.0-preview.3.8149! memory-leak 💦 Memory usage grows / objects live forever (sub: perf)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Multi-window memory leak
6 participants