Bug 38399 - Different GC behavior if called on ui thread / not on UI thread? (Was Possible memory leak in delegate usage with NSWindowController/NSWindow?)
Summary: Different GC behavior if called on ui thread / not on UI thread? (Was Possibl...
Status: RESOLVED ANSWERED
Alias: None
Product: Runtime
Classification: Mono
Component: GC ()
Version: unspecified
Hardware: PC Mac OS
: --- normal
Target Milestone: (C7)
Assignee: Aleksey Kliger
URL:
Depends on:
Blocks:
 
Reported: 2016-02-03 20:25 UTC by Chris Hamons
Modified: 2016-02-10 12:33 UTC (History)
5 users (show)

Tags:
Is this bug a regression?: ---
Last known good build:


Attachments
Example (360.26 KB, application/zip)
2016-02-03 20:25 UTC, Chris Hamons
Details
testcase (346.41 KB, application/zip)
2016-02-08 19:22 UTC, Rolf Bjarne Kvinge [MSFT]
Details


Notice (2018-05-24): bugzilla.xamarin.com is now in read-only mode.

Please join us on Visual Studio Developer Community and in the Xamarin and Mono organizations on GitHub to continue tracking issues. Bugzilla will remain available for reference in read-only mode. We will continue to work on open Bugzilla bugs, copy them to the new locations as needed for follow-up, and add the new items under Related Links.

Our sincere thanks to everyone who has contributed on this bug tracker over the years. Thanks also for your understanding as we make these adjustments and improvements for the future.


Please create a new report on GitHub or Developer Community with your current version information, steps to reproduce, and relevant error messages or log files if you are hitting an issue that looks similar to this resolved bug and you do not yet see a matching new report.

Related Links:
Status:
RESOLVED ANSWERED

Description Chris Hamons 2016-02-03 20:25:48 UTC
Created attachment 14872 [details]
Example

Steps to reproduce:
  - Open the attached project
  - Run with SHOW_LEAK uncommented
  - Click Open Window
  - Close new window
  - Repeat a few times
  - Note the number in the application output goes up
  - Comment out SHOW_LEAK to use NSNotificationCenter instead of delegate
  - Repeat, note we stay at 1-2 range
Comment 2 Rolf Bjarne Kvinge [MSFT] 2016-02-04 10:55:53 UTC
This is strange.

First you need to dispose the controller in the Clean function:

   _controller.Dispose ();

But that's not all: the NSWindow is still retained by the NSNotification object passed to the WillClose method.

So the question becomes: why isn't the NSNotification object collected by the GC?

I have no idea. The funny thing is that if you call GC.Collect on the background thread instead of the main thread the NSNotification object is collected (and then the NSWindow object is collected as well):

>	new System.Threading.Thread (() => {
>		while (true) {
>			System.Threading.Thread.Sleep (500);
>			System.GC.Collect ();
>			NSApplication.SharedApplication.BeginInvokeOnMainThread (() =>
>			{
>				System.Console.WriteLine ("GC {0}", NSApplication.SharedApplication.Windows.Length);
>			});
>		}
>	}).Start ();  

So apparently it matters on which thread GC.Collect is called. Maybe someone from the runtime team can explain this?

Another workaround that doesn't depend on the GC is to dispose the NSNotification object:

>	[Export ("windowWillClose:")]
>	public void WillClose (NSNotification notification)
>	{
>		Clean ();
>		notification.Dispose ();
>	}

Ugly, but it works.

Yet another possibility is to use IntPtr instead of NSNotification:

>	[Export ("windowWillClose:")]
>	public void WillClose (IntPtr notification)
>	{
>		Clean ();
>	}
Comment 5 Rolf Bjarne Kvinge [MSFT] 2016-02-08 19:21:43 UTC
@Rodrigo, for the GC issue, the only difference is whether the GC runs on the main thread or in a background thread.

This works:

>	new System.Threading.Thread (() => {
>		while (true) {
>			System.Threading.Thread.Sleep (500);
>			System.GC.Collect ();
>			NSApplication.SharedApplication.BeginInvokeOnMainThread (() =>
>			{
>				System.Console.WriteLine ("GC {0}", NSApplication.SharedApplication.Windows.Length);
>			});
>		}
>	}).Start ();  

this doesn't:

>	new System.Threading.Thread (() => {
>		while (true) {
>			System.Threading.Thread.Sleep (500);
>			NSApplication.SharedApplication.BeginInvokeOnMainThread (() =>
>			{
>				System.GC.Collect ();
>				System.Console.WriteLine ("GC {0}", NSApplication.SharedApplication.Windows.Length);
>			});
>		}
>	}).Start ();  

I can't see how that would make the app finalizer starved (nothing else really happens).

In any case I'm attaching a simplified test case.

Uncomment AppDelegate.cs:18 to make it work.
Comment 6 Rolf Bjarne Kvinge [MSFT] 2016-02-08 19:22:02 UTC
Created attachment 14949 [details]
testcase
Comment 7 Rodrigo Kumpera 2016-02-08 20:05:55 UTC
Hey Aleksey,

Do you mind investigating this one?
Comment 9 Rolf Bjarne Kvinge [MSFT] 2016-02-10 12:33:08 UTC
That's actually interesting, I think I know what's happening now:

* GC.Collect finds everything that needs to be collected. This includes the NSWindows. The objects are queued for finalization.
* NSApplication.SharedApplication.Windows is executed, which resurrects the windows, and they're thus not finalized.

This is another approach that works:

>	NSTimer.CreateRepeatingScheduledTimer (0.5, (v) =>
>	{
>		System.Console.WriteLine ("Windows {0}", NSApplication.SharedApplication.Windows.Length);
>	});
>	NSTimer.CreateRepeatingScheduledTimer (0.5 , (v) =>
>	{
>		System.GC.Collect ();	
>	});

So I'm closing this, there's no bug here.