One of the coolest tools in Apple’s Xcode tool chain is Instruments, the profiler.
Profiling is an essential debugging tool for every developer, whether you want to tune the performance of a particularly time-sensitive piece of code, or drill into some memory issues (be it leaks or general memory load). With ARC, just like with Garbage Collection on .NET or Java, regular object leaks are rare, but one scenario where they can still happen (opposed to GC) is with so-called “retain cycles” — where object A holds on to object B, and vice versa.
Because Instruments is such an essential tool for the Cocoa developer, we have deeply integrated support for it into the Oxygene “Nougat” tool chain as well, and i’d like to demonstrate that in a quick (only somewhat contrived) sample.
Let’s say you have the following code:
Looks innocent enough. DummyData holds an array of DummyDataItems it initializes on creation; the code (naïvely) assumes the array and everything else to be released when the DummyData object itself goes out of scope.
|type DummyData =classprivate fData: NSMutableArray;publicmethod init: id;override;method Work;empty;end; DummyDataItem =classprivate fOwner: DummyData;publicproperty owner: DummyData read fOwner;method initWithOwner(aOwner: DummyData): id;end; implementation method DummyData.init: id;beginself:=inherited init;ifassigned(self)thenbegin fData :=new NSMutableArray;for i: Int32 :=0to1000do fData.addObject(new DummyDataItem withOwner(self)); leak end;result:=self;end; method DummyDataItem.initWithOwner(aOwner: DummyData): id;beginself:=inherited init;ifassigned(self)thenbegin fOwner := aOwner;end;result:=self;end;
Except it doesn’t, and your customer calls to complain that the app’s memory footprint is growing. How do you find out what’s going on? Instruments to the rescue.
In Oyxgene “Nougat” (as of BETA 3), Instruments is available right from inside Visual Studio. We’ve added a new menu item to the Debug menu (and you can also add it to the toolbar of course): Start With Instruments:
Hit that and Oxygene will build your app (if necessary), and via the magic of CrossBox, you’ll see Instruments popping up, Mac side — by default asking you what kind of analysis you want to perform:
Select Leaks and that will open an Instruments document, and also start your application running. Play around with the app and trigger the code paths that lead to the memory increase. In the Instruments window, you san see what’s happening, live — the overall memory load of the app keeps increasing (as shown in the Allocations instrument):
Quitting the app and selecting the Leaks instrument shows all the memory that was leaked — that is, not properly released. The picture is quite clear — it seems that 31 DummyData instances were created and never properly released. What’s up with that? After all, your code that creates DummyData is dead simple:
“d” goes out of scope right after it’s used, and that should release the object, right?
|method MainWindowController.buttonClick(aSender: id);beginvar d :=new DummyData; d.Work();end;
Fold open one of the DummyData items in the list and click on the little arrow next to its address to drill into its retain/release history. You’ll see a huge list of roughly a thousand calls to retain. The call stack on the right tells you these happen from within “DummyDataItem.initWithOwner:”. That makes sense — your code creates a thousand of them, after all.
At the very end of the list, you see that from “buttonClick” your DummyData is being released though.
What’s going on? Shouldn’t “d” going out of scope release the array, which in turn releases the DummyDataItems, which in turn… wait, we’re getting close to the problem! It looks like our data structure contains what is called a “retain cycle”. The DummyData holds on to the NSArray, which holds on to the DummyDataItems which, in turn, hold on to the DummyData itself. Even though “d” is going out of scope, its retain count is only going down to 1001, because all the DummyDataItems still have references. As a result, the DummyData object actually never gets freed, and neither does the NSArray or the DummyDataItems inside it, which, in turn, can never give up their hold on the DummyData itself.
Though in this case we found the issue fairly quickly, Instruments has one more tool up its sleeve to make it even easier to find retain cycles: Click on the Leaks item in the navigation bar and select Cycles & Roots:
Instruments has actually detected any retain cycles for us and shows them in a list (in this case, 31 of the same), along with a nice graphical representation of what is going on.
From this view (even without our previous investigation), it becomes immediately clear that the fOwner reference from DummyDataItem back to DummyData is the culprit.
How do you break this vicious circle (assuming you cannot simply drop the owner reference altogether)? Weak references to the rescue!
By default, all variables and fields in Oxygene (and Objective-C with ARC) are strong — that means when an object is stored in the variable, its retain count is increased. By contrast, weak references just store the object, without affecting retain count. In fact, they do one better: they also keep track of the referenced object and automatically get set to nil when said object is released — so you never have to worry about the variable pointing to a stale object (which is a big problem in non-ARC languages).
|type DummyDataItem =classprivate fOwner: weak DummyData; …
Sidebar: A third type of object references are so-called unretained references. These behave like regular pointers in old-school languages; they store the object address, and when the object gets released that address will be stale — your code will be responsible for worrying about that.
With the code fixed, hit the Start With Instruments menu again. Your app will launch and Instruments will profile, and as you work with your app, you will notice that the memory load now stays down — as originally expected.
Of course, the Leaks pane will remain empty, but just to confirm, you can select the Allocations instrument, select Created & Destroyed in the sidebar and then locate and drill into one of the DummyData objects. As you can see, the retain/release history is much more sane now — no 1000 extra retains from DummyDataItem — and the object actually was released at the end of “buttonClick”.
So what have we seen in this article? We’ve had a quick look at how Instruments works and can be used to inspect memory allocations (the first phase of the investigation above does not just apply to bona-fide leaks and retain cycles, but can also be helpful if you just want to get a general impression of what memory your app is holding on to, and why), learned about retain cycles and strong, weak and unretained object references and we have also seen how Instruments can be used from Oxygene Nougat (which was so dead simple, it hardly warranted mentioning, wasn’t it?).