I have encountered a memory "leak" that I don't understand. I have a caching class whose purpose is to limit the number of instances of heavy Data items. Even when it stores NO instances of those items, they are being retained somehow. If they are from a background thread, I believe they are released upon completion of the task. But, of course, as demonstrated by the code, on the main thread they persist.
This occurs on iPadOS 14.8, iPadOS 15 and MacCatalyst.
What am I missing or don't understand?
class DataCache {
var id: String
var data: Data? {
get {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("\(id).txt")
return try! Data(contentsOf: url)
}
set {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("\(id).txt")
try! newValue!.write(to: url)
}
}
init(id: String, data: Data) {
self.id = id
self.data = data
}
}
class Item {
static var selection = [Item]()
static var items = [String:Item]()
var id: String = UUID().uuidString
var itemData: DataCache
init() {
itemData = DataCache(id: id,
data: Data(String(repeating: "dummy", count: 4_000_000).utf8)
)
}
required init(_ other: Item) {
self.itemData = DataCache(id: self.id, data: other.itemData.data!)
}
func duplicate(times: Int) {
for index in 0..<times {
print(index)
Item.selection.append(Item(self))
}
}
}
@main struct Main {
static func main() throws {
let item = Item()
perform(item)
performOnSelection() { item in
let _ = Item(item)
}
while (true) {}
}
static func perform(_ item: Item) {
item.duplicate(times: 100)
}
static func performOnSelection(perform action: @escaping (Item)->Void) {
var done = false
DispatchQueue.global().async {
for item in Item.selection {
action(item)
}
done = true
}
while !done { sleep (1) }
}
}
CodePudding user response:
You have autorelease objects being created. Insert an autoreleasepool
to drain the pool periodically, e.g.:
func performOnSelection(perform action: @escaping (Item) -> Void) {
...
autoreleasepool {
perform(item)
}
performOnSelection() { item in
autoreleasepool {
let _ = Item(item)
}
}
...
}
and
func duplicate(times: Int) {
for index in 0..<times {
print(index)
autoreleasepool { [self] in
Item.selection.append(Item(self))
}
}
}
E.g. without autoreleasepool
:
And with:
It turns off to be much faster when it is not dealing with all of those lingering autorelease objects, too. And peak memory usage has gone from 3.4gb to 47mb.