Memory leak in Swift’s FileHandle.read or Data buffer on macOS

Total
12
Shares

I try to to read a file on macOS with Swift using class FileHandle and function

@available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *)
public func read(upToCount count: Int) throws -> Data?

This works fine in principle. However, I’m faced with a memory leak. The data allocated in the returned Data buffer is not freed. Is this possibly a bug in the underlying Swift wrapper?

Here is my test function, which reads a file block by block (and otherwise does nothing with the data). (Yes, I know, the function is stupid and has the sole purpose to prove the memory leak issue.)

func readFullFile(filePath: String) throws {
    let blockSize = 32 * 1024
    guard let file = FileHandle(forReadingAtPath: filePath) else {
        fatalError("Failed to open file")
    }
    while (true) {
        guard let data = try file.read(upToCount: blockSize) else {
            break
        }
    }
}

If I call the function in a loop, and watch the program’s memory consumption, I can see that the memory goes up in each loop step by the size of the read file and is never released.

Can anybody confirm this behavior or knows how to fix it?

My environment:

  • macOS 11.3.1 Big Sur
  • XCode 12.5

Best,
Michael


Solution

The underlying Objective C implementation uses autorelease. Objects are kept alive by the thread’s auto-release pool. Typically this pool is drained on every iteration of the run loop.

But in your context, since you have a tight loop that runs multiple times within a single iteration of the run loop, you’re accumulating a bunch of objects that are kept around temporarily.

Rest assured, they will eventually be reallocated when the run-loop iteration completes, assuming your app doesn’t crash from EOM before that.

If the build-up of objects is an issue, you can manually drain the autorelease pool by placing the allocations inside an autorelease pool block.

for _ in something {
    autoreleasepool {
        // Do your work here
    }
}

Beware: if your work doesn’t allocate much memory, this might actually make performance worse. It’ll slow down your loop without much memory benefit.

Docs: ObjectiveC.autoreleasepool(invoking:)

To access this, you can import ObjectiveC, although more commonly you’ll have it transitively imported when you import Foundation, AppKit, Cocoa, etc.

Leave a Reply

Your email address will not be published. Required fields are marked *