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

DetachContextMenuStrip pattern can lead to memory leaks #12353

Open
1 of 2 tasks
kirsan31 opened this issue Oct 18, 2024 · 14 comments
Open
1 of 2 tasks

DetachContextMenuStrip pattern can lead to memory leaks #12353

kirsan31 opened this issue Oct 18, 2024 · 14 comments
Assignees
Labels
🚧 work in progress Work that is current in progress tenet-performance Improve performance, flag performance regressions across core releases
Milestone

Comments

@kirsan31
Copy link
Contributor

kirsan31 commented Oct 18, 2024

.NET version

All up to .Net9.

Did it work in .NET Framework?

No

Did it work in any of the earlier releases of .NET Core or .NET 5+?

No.

Issue description

This null out ContextMenuStrip pattern:

private void DetachContextMenuStrip(object? sender, EventArgs e) => ContextMenuStrip = null;

// And ContextMenuStrip.Set` property:
EventHandler disposedHandler = new(DetachContextMenuStrip);

if (oldValue is not null)
{
    oldValue.Disposed -= disposedHandler;
}

if (value is not null)
{
    value.Disposed += disposedHandler;
}

Can lead to memory leaks.
Using in 3 places:

  • Issue 1.

Control

More or less justified, since control in most cases lives either as long as the menu, or longer. But I didn't find a ContextMenuStrip.Disposed -= disposedHandler; code in Control.Dispose. So if ContextMenuStrip will outlive control (and user not null out ContextMenuStrip property) - we will have memory leak (control will remain in memory) at any case.

  • Issue 2.

DataGridViewBand and DataGridViewCell

Here it probably does more harm than good, since DataGridView elements in most cases live less than the menu and they never disposed.

My thots are:

  • At the very least we should add ContextMenuStrip.Disposed -= disposedHandler; code in Control.Dispose.
  • Ideally, to avoid making breaking changes we should use some implementation of weak events here. 🤔

Steps to reproduce

DetachContextMenuStripLeaks.zip

  • Use context menu on the DataGridView from the repro app, compare snapshots before and after:
  • "Refresh data" will leak DataGridView elements.
  • Every "Add Control" + "Remove Control" will leak one Control class.
@kirsan31 kirsan31 added the untriaged The team needs to look at this issue in the next triage label Oct 18, 2024
@elachlan elachlan added the tenet-performance Improve performance, flag performance regressions across core releases label Oct 19, 2024
@elachlan
Copy link
Contributor

@LeafShi1 can your team please test this?

@kirsan31
Copy link
Contributor Author

kirsan31 commented Oct 19, 2024

@elachlan DataGridView case (point 2) was tested in #6859.
I will adapt repro app to test also point 1...

---------------------UPD---------------------

Updated 1 post with repro app and instructions.

@LeafShi1
Copy link
Member

@LeafShi1 can your team please test this?

This problem does exist
Image

@JeremyKuhne JeremyKuhne added this to the .NET 10.0 milestone Oct 22, 2024
@JeremyKuhne JeremyKuhne removed the untriaged The team needs to look at this issue in the next triage label Oct 22, 2024
@LeafShi1 LeafShi1 self-assigned this Oct 23, 2024
@LeafShi1
Copy link
Member

LeafShi1 commented Oct 23, 2024

Before executing Dispose or bind new data to the datagridview, setting the control's ContextMenuStrip property to null seems to work.

Image

@kirsan31
Copy link
Contributor Author

Before executing Dispose or bind new data to the datagridview, setting the control's ContextMenuStrip property to null seems to work.

The funniest thing is that this whole story with Disposed event was invented so that the user would not do intuitively understandable things like nullout ContextMenuStrip property of the control before disposing the menu 🤷
But in the end this led to that the user having to do completely unintuitive things like nullout ContextMenuStrip property of the control before disposing the control itself 🤔

@LeafShi1
Copy link
Member

LeafShi1 commented Oct 24, 2024

The cause of this problem is that when setting the ContextMenuStrip property for the control, the ContextMenuStrip's Disposed event is subscribed, but the subscription is not correctly unsubscribed.

if (value is not null)
{
    value.Disposed += disposedHandler;
}

The best time to unsubscribe should be when the control or the control it is attached to is about to be disposed, and the unsubscription logic has been added to the DataGridViewBands's Dispose logic,

 protected virtual void Dispose(bool disposing)
 {
     if (disposing)
     {
         ContextMenuStrip? contextMenuStrip = ContextMenuStripInternal;
         if (contextMenuStrip is not null)
         {
             contextMenuStrip.Disposed -= DetachContextMenuStrip;
         }
     }

but the control is not disposed actively in the sample project, so contextMenuStrip.Disposed -= DetachContextMenuStrip; has not been executed.
Manually adding dispose can ensure that the bound control is disposed successfully
Image

@kirsan31
Copy link
Contributor Author

@LeafShi1

Manually adding dispose can ensure that the bound control is disposed successfully

Yea, or null out ContextMenuStrip property like with Control. All of this is described in 1 post.
But once again this is not logical and not intuitive from the user's point of view 🤷‍♂️

@LeafShi1
Copy link
Member

@elachlan @JeremyKuhne What do you think about this issue?

@elachlan
Copy link
Contributor

A user is responsible for the lifespan of a control they create. But in this case, we aren't asking for the user to dispose of the ContextMenuStrip. Its the reference to it. That seems wrong, and the ContextMenuStrip property should be cleared when a row is removed from the datagridview.

@JeremyKuhne
Copy link
Member

For Control we should defensively set the ContextMenuStrip property to null in Dispose. Doing so will unhook whatever is there and remove the reference.

For existing code, we need to be careful that we don't dispose something that we've created that user's code might have a reference to.

@kirsan31
Copy link
Contributor Author

kirsan31 commented Oct 26, 2024

@JeremyKuhne what do you think about implementing this Disposed subscribe through some kind of weak event?
It seems to me that the implementation of a weak event for this particular internal case will be quite simple (I can try to do a PR)...

@JeremyKuhne
Copy link
Member

@JeremyKuhne what do you think about implementing this Disposed subscribe through some kind of weak event? It seems to me that the implementation of a weak event for this particular internal case will be quite simple (I can try to do a PR)...

@kirsan31 I'm not opposed in general. If someone wants to do this I'll entertain it, but be forewarned that I don't have a lot of bandwidth at the moment. :) Things I would want to see:

@kirsan31
Copy link
Contributor Author

@JeremyKuhne

I read this topic, so I suggested implementing it in a narrow specific case, where we can avoid problems of wide public implementation.
My thots are:
Simple List<WeakReference<EventHandler>> and main rule - subscribe only through delegates explicitly declared as class members.
That is, to make a class not for widespread use, but strictly to solve one old problem (maybe not to make a separate class at all?).

@JeremyKuhne
Copy link
Member

JeremyKuhne commented Oct 27, 2024

@kirsan31 I believe we should encapsulate what we do here. I pointed out the issue not to say that we need to solve it, just that we need to consider and reference it so that:

  1. Those looking at the issue are aware of a real need in .NET
  2. If something is ever added that we can potentially replace whatever we've done

Our helper doesn't need to solve all things, just the immediate need.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🚧 work in progress Work that is current in progress tenet-performance Improve performance, flag performance regressions across core releases
Projects
None yet
Development

No branches or pull requests

4 participants