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?
How can I troubleshoot why this is being kept alive?
setup
? It always surprises me to rediscover that that stuff sticks around until all the tests are over. – Delaunaysetup
, at all. By the way, this measurement was taken within the measure block, on like the 10th iteration (out of 100). – Inclinometerwindow.isReleasedWhenClosed = false
? Wouldn’t this make windows pile up? – Delaunaytrue
makeswindow.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