Bug 57540 - Memory Leak in AppKit.NSImage
Summary: Memory Leak in AppKit.NSImage
Status: RESOLVED UPSTREAM
Alias: None
Product: Xamarin.Mac
Classification: Desktop
Component: Runtime ()
Version: 3.4.0 (15.2)
Hardware: Macintosh Mac OS
: --- normal
Target Milestone: ---
Assignee: Bugzilla
URL:
Depends on:
Blocks:
 
Reported: 2017-06-16 04:10 UTC by Denis Rotanov
Modified: 2017-06-20 13:25 UTC (History)
3 users (show)

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


Attachments
testcase (1.81 MB, application/octet-stream)
2017-06-19 05:48 UTC, Denis Rotanov
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 Developer Community or GitHub 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 UPSTREAM

Description Denis Rotanov 2017-06-16 04:10:21 UTC
Using AppKit.NSImage(string filename) or NSImage.FromStream(stream) causes memory leak.

#Steps to Reproduce:

Prepare lots of big (1024x1024 max) png files and execute the following:

```
foreach (var path in imageFiles) {
  var img = new AppKit.NSImage(path);
  img.Dispose();
  GC.Collect();
}

// or

foreach (var path in imageFiles) {
  using (Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read)) {
    var img = new AppKit.NSImage.FromStream(stream);
    img.Dispose();
  }
  GC.Collect();
}
```

at this point memory won't be freed. It won't be freed until application exits. Our asset processing pipeline allocates about ~3Gb this way and crashes in the middle of the process.
Comment 1 Adam Hartley [MSFT] 2017-06-16 10:11:54 UTC
Hi Denis, would you be bale to provide a small sample project for this? Ideally we want to see exactly how this is occurring for you, so using the same files would be helpful. 

If you don't want the sample to be public, please provide a download link for it (e.g., Dropbox) and mark your comment as private.

Thanks!
Comment 2 Chris Hamons 2017-06-16 15:11:10 UTC
One thing to consider is that this may not be a memory leak. Depending on your setting for this:

https://developer.apple.com/documentation/appkit/nsimage/1519850-cachemode?language=objc

Cocoa may be caching these images forever.
Comment 3 Chris Hamons 2017-06-16 15:11:43 UTC
Try setting never for your test and see if it reproduces.
Comment 4 Denis Rotanov 2017-06-16 15:55:59 UTC
Thank you, Chris. I already stumbled upon cacheMode before submitting a bug. Setting it to never doesn't help though. I also wonder if creating/disposing those images on not a main thread could has anything to do with the issue.

Adam, I'll try to isolate the issue into separate project tomorrow.
Comment 5 Denis Rotanov 2017-06-19 05:48:01 UTC
Created attachment 22963 [details]
testcase

It took longer than expected to locate the issue.

```
			while (true) {
				using (var s = new System.IO.FileStream("image_with_xmp_metadata.png", System.IO.FileMode.Open)) {
					var nsimg = NSImage.FromStream(s);
					nsimg.CacheMode = NSImageCacheMode.Never;
					nsimg.Dispose();
					System.GC.Collect();
				}
				System.Threading.Thread.Sleep(1);
			}
```

above code shouldn't crash with "out of memory". But it does. The key is in the png file which is ~200x200px png image with xmp metadata. It's size is >7Mb. This file loads much slower then usual and takes about ~33Mb RAM which is never returned despite Dispose() and GC.Collect(). 

This memory would be freed once function returns and something happens internally inside xamarin, or native libs, but in our real life case there are just too many files to process before we can return. Infinite loop here is to demonstrate that fact.
Comment 6 Chris Hamons 2017-06-19 18:17:25 UTC
I believe this is the equivalent obj-c code:

https://gist.github.com/chamons/b559c3adc8881f0db340b3cf3da790b9

since 

and running that for a bit gives me:

PID   COMMAND      %CPU  TIME     #TH    #WQ  #PORT MEM    PURG   CMPR PGRP PPID STATE    
BOOSTS          %CPU_ME %CPU_OTHRS UID  FAULTS   COW    MSGSENT   MSGRECV
2450  57540        80.5  00:44.87 5      3    214   1832M+ 132K   0B   2450 2451 sleeping *0[18]          0.00000 0.00000    501  483379+  714    4838      964

Obj-c is acting exactly the same from what I can tell.

Please try that locally and see if you can reproduce. If so, then I'd recommending filing a Radar with Apple at https://developer.apple.com/bug-reporting/
Comment 7 Denis Rotanov 2017-06-20 00:27:22 UTC
Yeah, I can reproduce that one locally. So I'll be moving to apple bug reporting all right. Thank you so much.
Comment 8 Chris Hamons 2017-06-20 13:25:55 UTC
^