--- a/thumbcache.swift 2026-09-23 13:32:39 +++ b/thumbcache.swift 2026-09-23 13:32:39 @@ -1,23 +1,24 @@ -// EXERCISE 01 — BROKEN ON PURPOSE. Do not copy this shape into real code. +// EXERCISE 01 — REPAIRED. // // A thumbnail pipeline. Each Thumbnailer decodes one image and reports back // through a completion handler it stores ON ITSELF so the work can be retried. // -// Symptom as reported by QA: "memory climbs while browsing folders and never -// comes back down, even after we navigate away and the cache is emptied." +// The repair: the stored closure holds self WEAKLY, so the reference graph is +// no longer a cycle and ARC releases every Thumbnailer when the cache drops +// its last strong reference. // // The fixture browses ROUNDS folders in a row, because ONE round does not // distinguish the defect from healthy behaviour: free() returns memory to the // allocator, not to the OS, so a single round's footprint looks the same -// either way. Growth ACROSS rounds is the discriminating measurement. +// either way. Growth ACROSS rounds is the discriminating measurement, and it +// is what this repair flattens. // // Build and run: -// swiftc -swift-version 6 -O thumbcache.swift -o /tmp/thumb_broken -// /tmp/thumb_broken +// swiftc -swift-version 6 -O thumbcache.swift -o /tmp/thumb_fixed +// /tmp/thumb_fixed // -// This file compiles with ZERO warnings under Swift 6 strict concurrency, -// which is the point: the compiler has nothing to say about object lifetime. -// Every measurement is printed as key=value so a script can assert on it. +// The observable behaviour — record count and checksum — is unchanged. Only +// the lifetime is. Every measurement is printed as key=value. import Darwin @@ -54,8 +55,8 @@ let counters: Counters var pixels: [UInt8] - // Stored on self. The closure assigned to it captures self. That edge, - // plus this stored property, closes a cycle ARC cannot break. + // Stored on self, but the closure assigned to it captures self weakly, + // so this property no longer closes a cycle. var onComplete: (() -> Void)? init(index: Int, counters: Counters) { @@ -71,8 +72,11 @@ } func begin() { - // The retry handler needs the decoded pixels, so it reaches for self. - onComplete = { + // The retry handler needs the decoded pixels, so it reaches for self + // — but weakly. If the Thumbnailer is gone there is nothing left to + // retry, which is exactly the right semantics here. + onComplete = { [weak self] in + guard let self else { return } self.counters.checksum &+= UInt64(self.pixels[0]) &+ UInt64(self.index) self.counters.records += 1 }