I'm writing an app in Swift for macOS that superposes images. It works fine but when I do it a lot of time, memory goes through the roof.
I tried to create a small project to read one image, and write it 1000 times. The image is 15K. It takes the program 2.7 Gb memory. The actual output folder with the 1000 images is 14Mb.
I use the DispatchQueue.global to have the UI free to display the operation number.
If you have any idea ;)
Thanks, Nicolas
Here the code:
import SwiftUI
struct ContentView: View {
@State private var presentImageImporter = false
@State private var runningComputation = 1
@State private var inputImage: NSImage?
var body: some View {
VStack {
Button("Image") {
presentImageImporter = true
}.fileImporter(isPresented: $presentImageImporter, allowedContentTypes: [.png, .jpeg]) { result in
switch result {
case .success(let url):
if url.startAccessingSecurityScopedResource() {
inputImage = NSImage(byReferencing: url)
url.stopAccessingSecurityScopedResource()
}
case .failure(let error):
print(error)
}
}
Button("Compute 1000 read image") {
runComputations()
}
Divider()
Text("\(runningComputation)")
Divider()
}.frame(minWidth: 200, minHeight: 200)
}
func runComputations() {
let downloadDir = try! FileManager.default.url(
for: FileManager.SearchPathDirectory.downloadsDirectory,
in: FileManager.SearchPathDomainMask.userDomainMask,
appropriateFor: nil,
create: true)
let subFolder = downloadDir.appendingPathComponent("toto")
try! FileManager.default.createDirectory(at: subFolder, withIntermediateDirectories: true)
DispatchQueue.global(qos: .userInitiated).async {
for i in 0...1000 {
writePNG(inputImage!, url: subFolder.appendingPathComponent("image\(i).png"))
runningComputation += 1
}
}
}
}
func writePNG(_ image: NSImage, url: URL) {
let newRep = NSBitmapImageRep(data: image.tiffRepresentation!)
newRep!.size = image.size
let pngData = newRep!.representation(using: .png, properties: [:])
do {
try pngData!.write(to: url)
} catch {
print(error)
}
}
and the trace image
The problem is here:
for i in 0...1000 {
writePNG(inputImage!, url: subFolder.appendingPathComponent("image\(i).png"))
runningComputation += 1
}
This is creating a lot of temporary autoreleased objects without draining the autorelease pool. You'll notice that at the end of the run, the memory usage suddenly drops to 8MiB. That's when the default autorelease pool drains.
You'll want to wrap each iteration in its own autoreleasepool
block so that the temporary objects will be discarded sooner.
for i in 0...1000 {
autoreleasepool {
writePNG(inputImage!, url: subFolder.appendingPathComponent("image\(i).png"))
runningComputation += 1
}
}