Memory leak, despite no strong references?
Asked Answered
I

1

5

I'm doing a performance test to try to measure the rendering performance of an important NSOutlineView in my Mac app. In the process, I'm looping several times, creating the view, embedding it in a dummy window, and rendering it to an image. I'm generalizing a bit, but this is roughly what it looks like:

// Intentionally de-indented these for easier reading in this narrow page
class MyPerformanceTest: XCTestCase { reading
func test() { 
measure() {
// autoreleasepool {

    let window: NSWindow = {
        let w = NSWindow(
            contentRect: NSRect.init(x: 100, y: 100, width: 800, height: 1200),
            styleMask: [.titled, .resizable, .closable, .miniaturizable],
            backing: .buffered,
            defer: false
        )
        w.tabbingMode = .disallowed
        w.cascadeTopLeft(from: NSPoint(x: 200, y: 200))
        w.makeKeyAndOrderFront(nil)
        w.contentView = testContentView // The thing I'm performance testing
        return w
    }()

    let bitmap = self.bitmapImageRepForCachingDisplay(in: self.frame)
        .map { bitmap in
            self.cacheDisplay(in: self.frame, to: bitmap)
            return bitmap
        }

    let data = bitmap.representation(using: .png, properties: [:])!

    saveToDesktop(data, name: "image1.png") // Helper function around Data.write(to:). Boring.

    window.isReleasedWhenClosed = false // Defaults to true, but crashes if true.
    window.close()
    
// }
}
}
}

I noticed that this was building up memory usage. Each window allocated in each loop of my measure(_:) block was sticking around. This makes sense, because I don't have the main run loop running so the Thread's auto-release pool is never drained. I wrapped my entire measure block in a call to autoreleasepool block, and this was resolved. Using the memory graph debugger, I confirmed that there was only ever 1 window max, which would be the one from the current iteration. Great.

However, I found the my NSOutlineViews, their rows, and their row models were still sticking around. There were thousands of them, so it was really blowing up the memory usage.

I profiled it with the Leaks instrument in Instruments: no leaks.

Then I inspected the objects in the memory graph debugger. There were no obvious strong reference cycles, and all of the objects had cases similar to this example. It's an NSOutlineView (well, a dynamic NSKVONotifying_* subclass, but that doesn't matter), with only one strong reference from an ObjC block. But that block is only referenced, weakly, by one reference (the black line). Shouldn't this whole thing have been deallocated?

Memory graph debugger showing the object just described

How can I troubleshoot why this is being kept alive?

Inclinometer answered 26/4, 2021 at 22:58 Comment(7)
Are you setting up anything in setup? It always surprises me to rediscover that that stuff sticks around until all the tests are over.Delaunay
Nope, there's no setup, at all. By the way, this measurement was taken within the measure block, on like the 10th iteration (out of 100).Inclinometer
How about that window.isReleasedWhenClosed = false? Wouldn’t this make windows pile up?Delaunay
Anyway the right way really is probably to track retains/releases and use Instruments.Delaunay
@Delaunay Setting it to true makes window.close() crash :| (I couldn't figure out why) And it's not the windows piling up (they get cleared out at the end autorelease pool), it's the NSOutlineviews, (but not scroll or clip views). What instrument should I use to track retains/releases? I know about the allocations and leaks ones, but I didn't know there's instruments that specifically track retains/releases on a particular object. Is that a thing?Inclinometer
Yepper, added an answer describing it.Delaunay
Great, thanks! I'm at work-work now, I'll take a look later when I start my personal-work :DInclinometer
D
8

How can I troubleshoot why this is being kept alive?

Use Instruments.

Configure Instruments for the Allocations template. Before you start recording, under File > Recording Options, configure the Allocations template options to Record Reference Counts.

Record and pause. Select a region of the track to study. Find the type of object you want to study and hit the little right-arrow to reveal all objects of that type. Select one in the list. Next to the address, hit the little right-arrow.

You will now see a history of retains and releases along with a running reference count. Selecting a retain/release shows the calls stack on the right. Thus you can deduce the memory management history of this object.

enter image description here

Delaunay answered 27/4, 2021 at 19:38 Comment(5)
Holy shit that's incredible. I thought the allocations instrument could only track the first malloc and the eventual free I never knew about this. I know about that "recording options" panel, but I always forget to snoop there to find out about new features like this.Inclinometer
I'll keep the question open to new answers for a bit, but I'll accept this later. Poking around with this new power now, sweet!Inclinometer
How reliable would you say these automatic retain/release pairings are? Should I be skeptical and verify them all, or just take them for ground truth?Inclinometer
All you want to know is, who is retaining you that you didn't expect. The compiler is not going to make a mistake in the ARC pairings.Delaunay
By the way, the memory graph will also show you the malloc stack if you turn it on in the Scheme editor. But it will NOT show you the whole list of retains and releases; only Instruments can do that.Delaunay

© 2022 - 2024 — McMap. All rights reserved.