NSCollectionView memory problem

My environment is Xcode 7.2, OS X 10.10.5, and an ARC project.


I've been tracking a problem with an ever-expanding memory footprint for a Mac application that uses a NSCollectionView. I believe I now understand what's happening but haven't found a way to fix it.


To mimic my original problem, I created a test app which has the NSCollectionView's content refreshed by a timer every two seconds. If the app is left untouched (either as the foremost application or not), Instruments shows the number of NSCollectionViewItem objects, along with their views, and model objects continuously increasing.


(I can provide sample code if desired but it's all really simple with bindings between a collection view and an array controller, the controller and an array, and labels to the represented object fields.)


Here's a sample retain history for my NSCollectionViewItem subclass:

0 Malloc +1 1 00:42.242.076 Foundation _decodeObjectBinary
  Retain/Autorelease/Release (3)   00:42.242.093 Foundation _decodeObjectBinary
4 Retain +1 2 00:42.242.717 Foundation _decodeObjectBinary
5 Retain +1 3 00:42.242.718 Foundation -[NSKeyedUnarchiver _replaceObject:withObject:]
6 Release -1 2 00:42.242.718 Foundation _decodeObjectBinary
7 Retain +1 3 00:42.242.719 Foundation _decodeObjectBinary
8 Autorelease   00:42.242.721 AppKit -[NSCollectionViewItem copyWithZone:]
9 Retain +1 4 00:42.242.722 AppKit -[NSCollectionViewItem copyWithZone:]
10 Release -1 3 00:42.242.733 Foundation -[NSKeyedUnarchiver dealloc]
11 Release -1 2 00:42.242.742 Foundation -[NSKeyedUnarchiver dealloc]
  Retain/Release (4)   00:42.243.993 Foundation _NSSetObjectValueAndNotify
  Retain/Release (4)   00:42.244.032 Foundation _NSSetObjectValueAndNotify
20 Autorelease   00:42.244.117 AppKit -[NSCollectionView setContent:]
21 Retain +1 3 00:42.244.118 AppKit -[NSCollectionView _getItemsToDisplay]
22 Retain +1 4 00:42.246.179 AppKit -[NSCollectionView _displayItems:withConfiguration:animate:]
23 Retain +1 5 00:42.246.903 AppKit __60-[NSCollectionView _displayItems:withConfiguration:animate:]_block_invoke_2
24 Retain +1 6 00:42.247.334 QuartzCore +[CATransaction setCompletionBlock:]
25 Release -1 5 00:42.248.393 AppKit -[NSCollectionView _displayItems:withConfiguration:animate:]
  Release (2) -2   00:42.250.725 Foundation __NSFireTimer
28 Release -1 2 00:42.250.984 Foundation __NSFireTimer
29 Release -1 1 00:42.255.196 AppKit _DPSNextEvent
30 Retain +1 2 00:44.242.063 AppKit -[NSCollectionView _displayItems:withConfiguration:animate:]
31 Release -1 1 00:44.242.066 AppKit -[NSCollectionView _displayItems:withConfiguration:animate:]
32 Retain +1 2 00:44.242.311 AppKit -[NSCollectionView _displayItems:withConfiguration:animate:]
33 Release -1 1 00:44.244.136 Foundation __NSFireTimer
34 Release -1 0 00:44.544.908 AppKit __49-[NSCollectionView _scheduleEndOfAnimationTimer:]_block_invoke
35 Retain +1 2 00:44.544.908 AppKit -[NSAutounbinder retainBindingTargetAndUnbind]


If an event such as mouse down or mouse moved is delivered to the application, the accumulated memory is released, as in:

36 Release -1 0 02:36.040.659 AppKit -[NSAutounbinder dealloc]
37 Free 0 02:36.040.669 AppKit -[NSResponder dealloc]


Obviously, telling users to keep the app active and shake the mouse now and then isn't a great solution. 🙂 Is there a way this is supposed to work?


(My attempts to send a fake mouse event to my own app at the end of the timer cycle haven't worked. Perhaps I'm doing it wrong; perhaps something is smart enough to know it's not an external event.)

Replies

It might be interesting to look at your sample project, if you can put it on Dropbox or somewhere.


There are some anomalies in your retain history, but it's not clear whether they represent anything strange or are just artefacts of the way Instruments displays output. For example, If the second number in each detail line is the object's retain count, then it's showing up as 0 on line 26 (i.e. 34), which ought to be impossible.


It looks like the memory accumulation is related to removing of KVO observers. It's possible that this could be your fault if you create your own observers and fail to remove them properly, or if you have a property that isn't being updated KVO compliantly (so that when it changes to nil as collection view structure is torn down every 2 seconds, its UI observer isn't notified and doesn't remove its observation).

A zip of the project is on Dropbox at www.dropbox.com/s/6x3fecrat9yxrkw/Collection%20copy.zip?dl=0.


If you'd like to see what I'm describing, Profile the application and observe PersonItem, PersonView, and PersonModel objects.


(Note again that the memory growth is cleared by interacting with the app when it's front.)


EDIT:

Let's see if breaking the URL scheme makes it acceptable.

Removing "https" didn't quite work...needed to re-paste as plain text too.

I've attempted to provide a Dropbox URL but my response is "Currently being moderated" for the past eight hours.


(Just to let you know I appreciate your interest and haven't wandered away.)

It'll be moderated from now until Cleopatra returns to rule over Egypt.


I believe you can get it out of moderation by removing the scheme (h t t p s : / /) from the link, so that it's not recognized as a link in the post. Anyone who's being cautious will copy/paste the link text instead of just clicking on it anyway.

Thank you. That led me in the right direction.

I don't have a good answer. My guess is that it's an obscure problem with CoreAnimation. I tested on 10.11.3 with Xcode 7.3 beta 5.


First, I see the behavior you describe. When the app is in the state of accumulating unused objects, it's temporarily leaking just about everything in the view hierarchy under the collection view, including the item views (PersonView in your case, though you didn't really need a custom class for that). It really does look like the work of unbinding the item view components has been deferred, and doesn't happen unless something triggers it. The observations keep everything else alive.


Second, if you use Mark Generation with the Allocations instrument, you can see that about 1000 objects are leaked about every 10 seconds. If you mark a few generations, then switch back to the app, you'll see objects (correctly) disappear from the previous generations, as they are properly released and collected. However, not all objects are released, about 200 remain. These are all very small objects related to the Core Animation behavior associated with replacing the contents of the collection view. (Look at the ones that are 32 bytes, and you'll see they're created when you replace the "people" array in the timer method.) I don't know why these hang around, and I don't think they're causing the other temporary leaks, but they may all be caused by the same thing, whatever that is.


I also took a long, hard look at your KVO compliance and proper thread usage (i.e. staying on the main thread), and it all looks correct — though your setup of the indexed collection property ("people") is a bit messy, and is not what I would recommend, for unrelated reasons. (For example, in the setter, take a copy of the array passed in, or, even better, re-use your original array by removing all the objects and adding in the objects from the parameter. Also, don't expose such properties publicly as NSMutableArray. Always use NSArray. Clients can use the indexed accessors (insert…/remove… and variants), or the mutable array proxy. Or have a "mutablePeople" (readonly!) NSMutableArray property that returns the mutable array proxy.)


I have two vague theories about why mouse movement triggers the memory collection. One is that when there is no user activity, your main event loop may try to run optimally for power management, resulting in not all deferred activity happening until something else happens. Another is that Apple has introduced some new weak reference counting behavior with Swift. If this is being used internally, there does seem to be the possibility of continued existence of unreferenced objects.


My suggestion is to avoid an app design that repeatedly replaces all of the content of the collection view. Instead, do individual updates on items. It's going to perform better anyway. Also, the amounts of memory being leaked are pretty small, so you could reasonably ignore the problem, at least while you take the issue up with Apple.


I suggest you start with a bug report, attaching your sample project. If this disappears forever into the Big Black Bug Report Hole, you could try using a TSI (tech support incident) to get an Apple engineer looking at your code with you. (They won't charge you if they can't help you, and if they do help you it's definitely worth the cost.)

Thanks for the analysis.


The code you saw came about because of a large memory accumulation in a complex application. I thought that reducing it to a simple case that didn't have memory problems would let me start over clean on that specific feature. Apparently, that's not happening. Some of what I'm doing in the sample is intentionally artificial for the purpose of simplification or testing. (e.g. PersonView exists to confirm which view really was accumulating.)


The real application can have a few hundred items in the collection, each of which has two scaled images, a couple of other icons, and assorted bits of text. It grows by multiple megabytes per minute while idling.


I tried using a remove/insert sequence rather than array replacement but saw no change in memory behaviour. (As you say, it might be better for other reasons, but it doesn't change the growth problem.)


Again, thank you very much for the sanity check. Given our schedule, I think the TSI suggestion is excellent advice.

A final note on this for anyone who might be researching a similar problem....


DTS reviewed the problem, told me to keep monitoring the bug in case a fix happened, but that they had no workaround to offer at this time.

The TSI was credited back to our account.

One comment: having a DataSource Delegate and/or a NSCollectionViewDelegate attached to your post-legacy NSCollectionView is a bad idea; it needs to be in a different class/object. In my case, such a situation exhausted available memory with leaked objects.