swiftautomatic-ref-countinginstrumentsappkitnsoutlineview

Memory leak, despite no strong references?


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?


Solution

  • 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