swiftmacosperformancensimage

Swift read writing images take a lot of memory


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

Trace


Solution

  • 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
        }
    }