Using Swift, I need to read integers from a binary files but can't read whole files into memory because of their size. I have 61G bytes(7.7 billion Integers) of data written into a dozen files of various sizes. The largest is 18G bytes(2.2 billion Integers). Some of the files might be read completely into memory but the largest is greater than available RAM.
Insert File IO Rant Here.
I have written the code to write the file 10 Million bytes at a time and it works well. I wrote this as a class but none of the rest of the code is object oriented. This is not an App so there is no idle time to do memory cleanup. Here is the code:
class BufferedBinaryIO {
var data = Data(capacity: 10000000)
var data1:Data?
let fileName:String!
let fileurl:URL!
var fileHandle:FileHandle? = nil
var (forWriting,forReading) = (false,false)
var tPointer:UnsafeMutablePointer<UInt8>?
var pointer = 0
init?(forWriting name:String) {
forWriting = true
fileName = name
fileurl = URL(fileURLWithPath:fileName)
if FileManager.default.fileExists(atPath: fileurl.path) {
try! fileHandle = FileHandle(forWritingTo: fileurl)
if fileHandle == nil {
print("Can't open file to write.")
return nil
}
}
else {
// if file does not exist write data for the first time
do{
try data.write(to: fileurl, options: .atomic)
try fileHandle = FileHandle(forWritingTo: fileurl)
} catch {
print("Unable to write in new file.")
return nil
}
}
}
init?(forReading name:String) {
forReading = true
fileName = name
fileurl = URL(fileURLWithPath:fileName)
if FileManager.default.fileExists(atPath: fileurl.path) {
try! fileHandle = FileHandle(forReadingFrom: fileurl)
if fileHandle == nil {
print("Can't open file to write.")
return nil
}
}
else {
// if file does not exist write data for the first time
do{
try fileHandle = FileHandle(forWritingTo: fileurl)
} catch {
print("Unable to write in new file.")
return nil
}
}
}
deinit {
if forWriting {
fileHandle?.seekToEndOfFile()
fileHandle?.write(data)
}
try? fileHandle?.close()
}
func write(_ datum: Data) {
guard forWriting else { return }
self.data.append(datum)
if data.count == 10000000 {
fileHandle?.write(data)
data.removeAll()
}
}
func readInt() -> Int? {
if data1 == nil || pointer == data1!.count {
if #available(macOS 10.15.4, *) {
//data1?.removeAll()
//data1 = nil
data1 = try! fileHandle?.read(upToCount: 10000000)
pointer = 0
} else {
// Fallback on earlier versions
}
}
if data1 != nil && pointer+8 <= data1!.count {
let retValue = data1!.withUnsafeBytes { $0.load(fromByteOffset: pointer,as: Int.self) }
pointer += 8
// data.removeFirst(8)
return retValue
} else {
print("here")
}
return nil
}
}
As I said writing to the file works fine and I can read from the file but I have a problem.
Some of the solutions for reading binary and converting it to various types use code like:
let rData = try! Data(contentsOf: url)
let tPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: rData.count)
rData.copyBytes(to: tPointer, count: rData.count)
The first line reads in the whole file consuming a like amount of memory and the next two lines double the memory consumption. So even if I have 16G bytes of Ram I can only read an 8Gbyte file because it has to double consume memory.
As you can see my code does not use this code. For the read I just read the file into data1, 10 million bytes at a time, and then use data1 like it was a regular data type and access it and can read the data fine, without doubling the memory usage.
The code in the body of the program that uses this code looks like:
file loop .... {
let string = String(format:"~path/filename.data")
let dataPath = String(NSString(string: string).expandingTildeInPath)
let fileBuffer = BufferedBinaryIO(forReading: dataPath)
while let value = fileBuffer!.readInt() {
loop code
}
}
Here is my problem: This code works to read the file into Ints but inside readInt, the code does not release the memory from the previous fileHandle?.read when it does the next fileHandle?.read. So as I go through the file the memory consumption goes up 10 million each time it fills the buffer until the program crashes.
Forgive my code as it is a work in progress. I keep changing it to try out different things to fix this problem. I used data1 as an optional variable for the read portion of the code, thinking setting it to nil would deallocate the memory. It does the same thing when I just over write it.
That being said, this would be a nice way to code this if it worked.
So the question is do I have a memory retention cycle or is there a magic bean I need to use on data1 get it to stop doing this?
Thank you in advance for your consideration of this problem.
You don't show your code that actually reads from your file, so it's a bit hard to be sure what's going on.
From the code you did show we can tell you're using a FileHandle, which allows random access to a file and reading arbitrary-sized blocks of data.
Assuming you're doing that part right and reading 10 million bytes at a time, your problem may be the way iOS and Mac OS handle memory. For some things, the OS puts no-longer-used memory blocks into an "autorelease pool", which gets freed when your code returns and the event loop gets serviced. If you're churning through multiple gigabytes of file data synchronously, it might not get a chance to release the memory before the next pass.
(Explaining Mac OS/iOS memory management in enough detail to cover autoreleasing would be pretty involved. If you're interested, I suggest you look up Apple manual reference counting and automatic reference counting, a.k.a ARC, and look for results that explain what goes on "under the covers".)
Try putting the code that reads 10 million bytes of data into the closure of an autoreleasePool() statement. That will cause any autoreleased memory to actually get released. Something like the pseudo-code below:
while (more data) {
autoreleasepool {
// read a 10 million byte block of data
// process that block
}
}
Related
I'm trying to build a chunked file uploading mechanism using modern Swift Concurrency.
There is a streamed file reader which I'm using to read files chunk by chunk of 1mb size.
It has two closures nextChunk: (DataChunk) -> Void and completion: () - Void. The first one gets called as many times as there is data read from InputStream of a chunk size.
In order to make this reader compliant to Swift Concurrency I made the extension and created AsyncStream
which seems to be the most suitable for such a case.
public extension StreamedFileReader {
func read() -> AsyncStream<DataChunk> {
AsyncStream { continuation in
self.read(nextChunk: { chunk in
continuation.yield(chunk)
}, completion: {
continuation.finish()
})
}
}
}
Using this AsyncStream I read some file iteratively and make network calls like this:
func process(_ url: URL) async {
// ...
do {
for await chunk in reader.read() {
let request = // ...
_ = try await service.upload(data: chunk.data, request: request)
}
} catch let error {
reader.cancelReading()
print(error)
}
}
The issue there is that there is no any limiting mechanism I'm aware of that won't allow to execute more than
N network calls. Thus when I'm trying to upload huge file (5Gb) memory consumption grows drastically.
Because of that the idea of streamed reading of file makes no sense as it'd be easier to read the entire file into the memory (it's a joke but looks like that).
In contrast, if I'm using a good old GCD everything works like a charm:
func process(_ url: URL) {
let semaphore = DispatchSemaphore(value: 5) // limit to no more than 5 requests at a given time
let uploadGroup = DispatchGroup()
let uploadQueue = DispatchQueue.global(qos: .userInitiated)
uploadQueue.async(group: uploadGroup) {
// ...
reader.read(nextChunk: { chunk in
let requset = // ...
uploadGroup.enter()
semaphore.wait()
service.upload(chunk: chunk, request: requset) {
uploadGroup.leave()
semaphore.signal()
}
}, completion: { _ in
print("read completed")
})
}
}
Well it is not exactly the same behavior as it uses a concurrent DispatchQueue when AsyncStream runs sequentially.
So I did a little research and found out that probably TaskGroup is what I need in this case. It allows to run async tasks in parallel etc.
I tried it this way:
func process(_ url: URL) async {
// ...
do {
let totalParts = try await withThrowingTaskGroup(of: Void.self) { [service] group -> Int in
var counter = 1
for await chunk in reader.read() {
let request = // ...
group.addTask {
_ = try await service.upload(data: chunk.data, request: request)
}
counter = chunk.index
}
return counter
}
} catch let error {
reader.cancelReading()
print(error)
}
}
In that case memory consumption is even more that in example with AsyncStream iterating!
I suspect that there should be some conditions on which I need to suspend group or task or something and
call group.addTask only when it is possible to really handle these tasks I'm going to add but I have no idea how to do it.
I found this Q/A
And tried to put try await group.next() for each 5th chunk but it didn't help me at all.
Is there any mechanism similar to DispatchGroup + DispatchSemaphore but for modern concurrency?
UPDATE:
In order to better demonstrate the difference between all 3 ways here are screenshots of memory report
AsyncStream iterating
AsyncStream + TaskGroup (using try await group.next() on each 5th chunk)
GCD DispatchQueue + DispatchGroup + DispatchSemaphore
The key problem is the use of the AsyncStream. Your AsyncStream is reading data and yielding chunks more quickly than it can be uploaded.
Consider this MCVE where I simulate a stream of 100 chunks, 1mb each:
import os.log
private let log = OSLog(subsystem: "Test", category: .pointsOfInterest)
struct Chunk {
let index: Int
let data: Data
}
actor FileMock {
let maxChunks = 100
let chunkSize = 1_000_000
var index = 0
func nextChunk() -> Chunk? {
guard index < maxChunks else { print("done"); return nil }
defer { index += 1 }
return Chunk(index: index, data: Data(repeating: UInt8(index & 0xff), count: chunkSize))
}
func chunks() -> AsyncStream<Chunk> {
AsyncStream { continuation in
index = 0
while let chunk = nextChunk() {
os_signpost(.event, log: log, name: "chunk")
continuation.yield(chunk)
}
continuation.finish()
}
}
}
And
func uploadAll() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
let chunks = await FileMock().chunks()
var index = 0
for await chunk in chunks {
index += 1
if index > 5 {
try await group.next()
}
group.addTask { [self] in
try await upload(chunk)
}
}
try await group.waitForAll()
}
}
func upload(_ chunk: Chunk) async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: #function, signpostID: id, "%d start", chunk.index)
try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: #function, signpostID: id, "end")
}
When I do that, I see memory spike to 150mb as the AsyncStream rapidly yields all of the chunks upfront:
Note that all the Ⓢ signposts, showing when the Data objects are created, are clumped at the start of the process.
Note, the documentation warns us that the sequence might conceivably generate values faster than they can be consumed:
An arbitrary source of elements can produce elements faster than they are consumed by a caller iterating over them. Because of this, AsyncStream defines a buffering behavior, allowing the stream to buffer a specific number of oldest or newest elements. By default, the buffer limit is Int.max, which means the value is unbounded.
Unfortunately, the various buffering alternatives, .bufferingOldest and .bufferingNewest, will only discard values when the buffer is filled. In some AsyncStreams, that might be a viable solution (e.g., if you are tracking the user location, you might only care about the most recent location), but when uploading chunks of the file, you obviously cannot have it discard chunks when the buffer is exhausted.
So, rather than AsyncStream, just wrap your file reading with a custom AsyncSequence, which will not read the next chunk until it is actually needed, dramatically reducing peak memory usage, e.g.:
struct FileMock: AsyncSequence {
typealias Element = Chunk
struct AsyncIterator : AsyncIteratorProtocol {
let chunkSize = 1_000_000
let maxChunks = 100
var current = 0
mutating func next() async -> Chunk? {
os_signpost(.event, log: log, name: "chunk")
guard current < maxChunks else { return nil }
defer { current += 1 }
return Chunk(index: current, data: Data(repeating: UInt8(current & 0xff), count: chunkSize))
}
}
func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator()
}
}
And
func uploadAll() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
var index = 0
for await chunk in FileMock() {
index += 1
if index > 5 {
try await group.next()
}
group.addTask { [self] in
try await upload(chunk)
}
}
try await group.waitForAll()
}
}
And that avoids loading all 100mb in memory at once. Note, the vertical scale on memory is different, but you can see that the peak usage is 100mb less than the above graph and the Ⓢ signposts, showing when data is read into memory, are now distributed throughout the graph rather than all at the start:
Now, obviously, I am only mocking the reading of a large file with Chunk/Data objects and mocking the upload with a Task.sleep, but it hopefully illustrates the basic idea.
Bottom line, do not use AsyncStream to read the file, but rather consider a custom AsyncSequence or other pattern that reads the file in as the chunks are needed.
A few other observations:
You said “tried to put try await group.next() for each 5th chunk”. Perhaps you can show us what you tried. But note that this answer didn’t say “each 5th chunk” but rather “every chunk after the 5th”. We cannot comment on what you tried unless you show us what you actually tried (or provide a MCVE). And as the above shows, using Instruments’ “Points of Interest” tool can show the actual concurrency.
By the way, when uploading large asset, consider using a file-based upload rather than Data. The file-based uploads are far more memory efficient. Regardless of the size of the asset, the memory used during a file-based asset will be measured in kb. You can even turn off chunking entirely, and a file-based upload will use very little memory regardless of the file size. URLSession file uploads have a minimal memory footprint. It is one of the reasons we do file-based uploads.
The other reason for file-based uploads is that, for iOS especially, one can marry the file-based upload with a background session. With a background session, the user can even leave the app to do something else, and the upload will continue to operate in the background. At that point, you can reassess whether you even need/want to do chunking at all.
I would like to use some C code that uses a file descriptor.
Background is that I would like to read some data from cgraph library.
public extension UnsafeMutablePointer where Pointee == Agraph_t {
func saveTo(fileName: String) {
let f = fopen(cString(fileName), cString("w"))
agwrite(self,f)
fsync(fileno(f))
fclose(f)
}
}
I would like to have the file output, but without writing to a temp file. Hence, I would like to do something like this:
public extension UnsafeMutablePointer where Pointee == Agraph_t {
var asString: String {
let pipe = Pipe()
let fileDescriptor = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
fileDescriptor.pointee = pipe.fileHandleForWriting.fileDescriptor
agwrite(self, fileDescriptor)
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8) {
return output
}
return ""
}
}
But it doesn't work, resulting in a EXC_BAD_ACCESS within agwrite(,). What do I need to do instead?
Many thanks in advance!
File descriptors and file pointers are not the same thing. It's confusing, and made even more frustrating by the fact that FILE * is really hard to Google because of the symbol.
You need to fdopen the file descriptor (pipe.fileHandleForWriting.fileDescriptor), to receive a FILE * (UnsafeMutablePointer<FILE> in Swift). This is what you then pass to agwrite.
It's important to fclose the file pointer when you're done writing to it, otherwise .readDataToEndOfFile() will never terminate. I made a helper function to ensure the fclose can't be forgetten. It's possible that agwrite closes the file pointer itself, internally. If that's the case, you should delete this code and just give it the result of fdopen, plain and simple.
import Foundation
public typealias Agraph_t = Int // Dummy value
public struct AGWriteWrongEncoding: Error { }
func agwrite(_: UnsafeMutablePointer<Agraph_t>, _ filePointer: UnsafeMutablePointer<FILE>) {
let message = "This is a stub."
_ = message.withCString { cString in
fputs(cString, stderr)
}
}
#discardableResult
func use<R>(
fileDescriptor: Int32,
mode: UnsafePointer<Int8>!,
closure: (UnsafeMutablePointer<FILE>) throws -> R
) rethrows -> R {
// Should prob remove this `!`, but IDK what a sensible recovery mechanism would be.
let filePointer = fdopen(fileDescriptor, mode)!
defer { fclose(filePointer) }
return try closure(filePointer)
}
public extension UnsafeMutablePointer where Pointee == Agraph_t {
func asString() throws -> String {
let pipe = Pipe()
use(fileDescriptor: pipe.fileHandleForWriting.fileDescriptor, mode: "w") { filePointer in
agwrite(self, filePointer)
}
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let output = String(data: data, encoding: .utf8) else {
throw AGWriteWrongEncoding()
}
return output
}
}
let ptr = UnsafeMutablePointer<Agraph_t>.allocate(capacity: 1) // Dummy value
print(try ptr.asString())
Several other things:
Throwing an error is probably a better choice than returning "". Empty strings aren't a good error handling mechanism. Returning an optional would also work, but it's likely to always be force unwrapped, anyway.
readDataToEndOfFile is a blocking call, which can lead to a bad use experience. It's probably best that this code be run on a background thread, or use a FileHandle.readabilityHandler to asynchronously consume the data as it comes in.
I am experiencing a memory leak when reading data from files. This code creates the leak:
func read() throws {
let url = URL(fileURLWithPath: "content.pdf")
let fileHandle = try FileHandle(forReadingFrom: url)
while true {
let chunk = fileHandle.readData(ofLength: 256)
guard !chunk.isEmpty else {
break
}
}
print("read")
}
do {
for _ in 0 ..< 10000 {
try read()
}
}
catch {
print("Error: \(error)")
}
*FYI: to run this code you will have to have a "content.pdf" file in your working directory.
If I run this on linux with Swift 3.1.1 (or 3.1), it does a number of iterations of the loop consuming more and more memory until the process is killed.
On Mac this also happens because the data is put into the Autorelease pool and I can fix the memory issue by wrapping each iteration in an autorelease pool but that does not exist on Linux so I don't know how I can free up that memory. Does anyone have an idea?
I found the problem which is within the standard library. There is actually already a bug report open for it. Basically the problem is that the readData(ofLength:) method is returning a Data object that is not cleaning up after itself when deallocated.
For now, I am using this workaround:
extension FileHandle {
public func safelyReadData(ofLength length: Int) -> Data {
#if os(Linux)
var leakingData = self.readData(ofLength: length)
var data: Data = Data()
if leakingData.count > 0 {
leakingData.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) -> Void in
data = Data(bytesNoCopy: bytes, count: leakingData.count, deallocator: .free)
})
}
return data
#else
return self.readData(ofLength: length)
#endif
}
}
Anywhere I was previously using readData(ofLength:) I am now using my safelyReadData(ofLength:) method. On all platforms other than Linux it simply calls the original because those implementations are fine. On Linux I am creating a copy of the data that will actually free the underlying data when deallocated.
Instead of how to work around the missing autorelease pool, a better question is how to prevent the leak. Maybe creating (and not deallocating) 10,000 FileHandles are the problem. Try this.
func read() throws {
let url = URL(fileURLWithPath: "content.pdf")
let fileHandle = try FileHandle(forReadingFrom: url)
while true {
let chunk = fileHandle.readData(ofLength: 256)
guard !chunk.isEmpty else {
break
}
}
fileHandle.closeFile()
print("read")
}
This may not be the problem, but it is still good code hygiene. How many loops are made before the crash?
I am writing a video app that records video only when triggered and at the end of all the recordings merges the recordings together into one at the end.
I was just wondering if there is process in swift to make sure the name of the next file of a recording is different than the previous one? I know ways of doing this that are fine, but I am a bit of a memory freak and was wondering if swift has a built in answer to this problem?
Edit:
This works with the variable filenamechanger. I just was wondering if there is an even better way.
var filenamechanger = 0
if motiondetected == true {
do{
let documentsDir = try FileManager.default.url(for:.documentDirectory, in:.userDomainMask, appropriateFor:nil, create:true)
filenamechanger += 1 //name changer
let fileURL = URL(string:"test\(filenamechanger).mp4", relativeTo:documentsDir)!
do {
try FileManager.default.removeItem(at:fileURL)
} catch {
}
self.movieOutput = try MovieOutput(URL:fileURL, size:Size(width:480, height:640), liveVideo:true)
self.camera.audioEncodingTarget = self.movieOutput
self.camera --> self.movieOutput!
self.movieOutput!.startRecording()
sleep(3)
self.camera.audioEncodingTarget = nil
self.movieOutput = nil
motiondetected = false
}
catch{
"recording didn't work"
}
}
I am running into some trouble importing a large .csv file in OS X using Core Data on a background thread.
My simplified data model is a Dataset which has a to-many relationship to many Entries. Each entry is a line in the .csv file (which has a bunch of attributes that I'll leave out for brevity). Following what I have read about efficiently importing lots of data, along with how to make a progress indicator work properly, I have created a managed object context for the purposes of going the import.
I am having trouble with two things:
I need to hang on to a reference to the new Dataset at the end of the import, because I need to select it in the popup. this will be done in the main thread, but for efficiency (and to make my NSProgressIndicator work) the new dataset is created on the background thread, with the background MOC.
From what I read, batching the import, so that the background MOC saves and resets, is the best way to stop the import from eating up too much memory. That does not turn out to be the case so far - it looks like gigs of memory is being used even for files in the tens of megabytes. Also, once I reset my import MOC, it cannot find the data for the inDataset, and so cannot create the relationship between all subsequent Entries and the Dataset.
I've posted the simplified code below. I have tried refreshObject:mergeChanges, without good results. Can anyone point me at what I am doing wrong?
let inFile = op.URL
dispatch_async(dispatch_get_global_queue(priority, 0)) {
//create moc
let inMOC = NSManagedObjectContext()
inMOC.undoManager = nil
inMOC.persistentStoreCoordinator = self.moc.persistentStoreCoordinator
var inDataset : inDataset = Dataset(entity: NSEntityDescription.entityForName("Dataset", inManagedObjectContext: inMOC)!, insertIntoManagedObjectContext: inMOC)
//set up NSProgressIndicator here, removed for clarity
let datasetID = inDataset.objectID
mocDataset = self.moc.objectWithID(datasetID) as! Dataset
let fileContents : String = (try! NSString(contentsOfFile: inFile!.path!, encoding: NSUTF8StringEncoding)) as String
let fileLines : [String] = fileContents.componentsSeparatedByString("\n")
var batchCount : Int = 0
for thisLine : String in fileLines {
let newEntry : Entry = Entry(entity: NSEntityDescription.entityForName("Entry", inManagedObjectContext: inMOC)!, insertIntoManagedObjectContext: inMOC)
//Read in attributes of this entry from this line, removed here for brevity
newEntry.setValue("Entry", forKey: "type")
newEntry.setValue(inDataset, forKey: "dataset")
inDataset.addEntryObject(newEntry)
dispatch_async(dispatch_get_main_queue()) {
self.progInd.incrementBy(1)
}
batchCount++
if(batchCount > 1000){
do {
try inMOC.save()
} catch let error as NSError {
print(error)
} catch {
fatalError()
}
batchCount = 0
inMOC.reset()
inDataset = inMOC.objectWithID(datasetID) as! Dataset
// fails here, does not seem to be able to find the data associated with inDataset
}
}// end of loop for reading lines
//save whatever remains after last batch save
do {
try inMOC.save()
} catch let error as NSError {
print(error)
} catch {
fatalError()
}
inMOC.reset()
dispatch_async(dispatch_get_main_queue()) {
//This is done on main queue after background queue has read all line
// I thought the statement just below would refresh mocDataset, but no luck
self.moc.refreshObject(mocDataset, mergeChanges: true)
//new dataset selected from popup
let datafetch = NSFetchRequest(entityName: "Dataset")
let datasets : [Dataset] = try! self.moc.executeFetchRequest(datafetch) as! [Dataset]
self.datasetController.addObjects(datasets)
let mocDataset = self.moc.objectWithID(datasetID) as! Dataset
//fails here too, mocDataset object has data as a fault
let nDarray : [Dataset] = [mocDataset]
self.datasetController.setSelectedObjects(nDarray)
}
}