1

The following task is given:

  • Scan a folder, which contains subfolders with images inside and ensure that none of the images is corrupted
  • Use MacOS and Swift
  • Open every image and check it against corruption

I wrote this tiny command line program:

import ArgumentParser
import AppKit
import Foundation

struct CheckImages: ParsableCommand {
    @Option(help: "The images root directory")
    var path: String

    func run() throws {
        let directories = try FileManager.default.contentsOfDirectory(atPath: path)

        for directory in directories {
            if directory == ".DS_Store" {
                continue
            }

            let prefix = self.path + "\(directory)/PREFIX_\(directory)"

            let imageName = prefix + ".jpg"
            let image = NSImage(contentsOfFile: imageName)
            if image == nil {
                print("PROBLEM \(imageName)")
            }
        }
    }
}

CheckImages.main()

Each image is around 20MB in size. Altogether I have ~150.000 images to check.

Unfortunately XCode terminates the program with Program ended with exit code: 9. Digging deeper (with Instruments) it turns out that this little help application consumes all my memory in NSImage.init(). As NSImage is a mature object, I doubt that there is any problem with it. Thus, my question is, can anybody explain this behaviour to me?

My environment:

  • XCode Version 11.4.1 (11E503a)
  • Apple Swift version 5.2.2 (swiftlang-1103.0.32.6 clang-1103.0.32.51)
Evel Knievel
  • 235
  • 3
  • 11
  • I never heard of ArgumentParser, but... you're doing stuff inside a struct. Check if there are any references to the struct itself inside closures used inside the struct. It's known to cause retain cycles. Structs are value types. NSImage is a references type. – I make my mark Apr 20 '20 at 05:20
  • This has nothing to do with `struct`s or so, I can strip down this code without the `ArgumentParser`. BTW `ArgumentParser` is the official Apple way to parse the command line arguments. – Evel Knievel Apr 20 '20 at 17:27

1 Answers1

2

Answering my own question, I need an autorelease pool here. As I was never programming in Objective-C I was not aware of such things as autorelease pools. As NSImage is 'just' a wrapper around the ObjC-NSImage object it needs an autorelease pool to manage the deallocation. We are in 2020 and we have to manager such things in this way?

In this post I found the answer to the above problem: Is it necessary to use autoreleasepool in a Swift program?

Another nice post can be found here: https://swiftrocks.com/autoreleasepool-in-2019-swift.html

So, the above code has to look like this:

import ArgumentParser
import AppKit
import Foundation

struct CheckImages: ParsableCommand {
    @Option(help: "The images root directory")
    var path: String

    func run() throws {
        let directories = try FileManager.default.contentsOfDirectory(atPath: path)

        for directory in directories {
            if directory == ".DS_Store" {
                continue
            }

            autoreleasepool {
                let prefix = self.path + "\(directory)/PREFIX_\(directory)"

                let imageName = prefix + ".jpg"
                let image = NSImage(contentsOfFile: imageName)
                if image == nil {
                    print("PROBLEM \(imageName)")
                }
            }
        }
    }
}

CheckImages.main()
Evel Knievel
  • 235
  • 3
  • 11