9

I am using the following tutorial to implement Core Data into my Swift IOS application. As shown in the video, my persistance manager is created via a singleton pattern. Here is the code that describes it:

import Foundation
import CoreData

class DataLogger {

    private init() {}
    static let shared = DataLogger()
    lazy var context = persistentContainer.viewContext

    private var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "mycoolcontainer")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    func save () {
        if self.context.hasChanges {
            self.context.perform {
                do {
                    try self.context.save()
                } catch {
                    print("Failure to save context: \(error)")
                }
            }
        }
    }
}

Now, if I create a loop with around 1000 or so elements of my entity ( MyEntity is the Core Data entity object ) with the following code, the application crashes.

class MySampleClass {

    static func doSomething {
        for i in 0 ..< 1000 {
            let entry = MyEntity(context: DataLogger.shared.context);
            // do random things
        }
        DataLogger.shared.save()
    }
}

It crashes on MyEntity(context: DataLogger.shared.context), and I am unable to observe any logs to see why. Occasionally, it will reach the save() call and succeed or crash with the the generic error that states:

Heap corruption detected, free list is damaged at 0x280a28240 *** Incorrect guard value: 13859718129998653044

I've tried to look around the net to find any hints as to what the issue could be. I've tried to make the save() method in the DataLogger synchronized, via .performAndWait(), and saw no success. I also tried to use childContexts to perform the same, with no success, via this code:

let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
childContext.parent = context
childContext.hasChanged {
    childContext.save();
}

I suspect that I am implementing the DataLogger class incorrectly but cannot observe what the actual issue might be. It might be related to the quantity of created objects, or possibly threading, but I am not certain. What would be the correct way to implement the DataLogger class to ensure any other class can use it and store the entities to disk?

andrewbuilder
  • 3,629
  • 2
  • 24
  • 46
angryip
  • 2,140
  • 5
  • 33
  • 67

3 Answers3

7

I ended up reading more on Core Data, and solving the problem this way:

First, I ended up moving the persistence container into the app delegate. It can be defined as follows:

 import UIKit
 import CoreData

 @UIApplicationMain
 class AppDelegate: UIResponder, UIApplicationDelegate { 

      ... applicationWillTerminate ....

      // add the core data persistance controller
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "mycoolcontainer")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container;
    }()

    func save() {
        let context = persistentContainer.viewContext;
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                print("Failure to save context: \(error)")
            }
        }
    }

 }

In every view controller that I need to access the context, I'd do something as follows:

import UIKit

class CoolViewController: UIPageViewController, UIPageViewControllerDelegate {

    let appDelegate = UIApplication.shared.delegate as! AppDelegate;

    func storeAndFetchRecords() {
        MySampleClass().doSomething(context: appDelegate.persistentContainer.viewContext);
        appDelegate.save()
    }

}

The classes that need to interact with the context, would do it as follows:

import CoreData

class MySampleClass {

    func doSomething(context: NSManagedObjectContext) {
        for i in 0 ..< 1000 {
            let entry = MyEntity(context: context);
            // do random things
        }
    }
}

Since this lives on the main thread, there is no need to run a "performAndWait". This would only make sense in the event if you initiated a background context, via this syntax:

appDelegate.persistentContainer.newBackgroundContext()
angryip
  • 2,140
  • 5
  • 33
  • 67
6

I've decided to add a second answer because the approach is different from my previous answer.

Some further reading on your problem indicates an issue with memory allocation.

In short, my assessment is...

  1. that your iteration for i in 0 ..< 1000 is formally a synchronous process but, due to the call to Core Data within, may be acting "out-of-synch" with Core Data processes. I suspect that whatever Core Data processes are underway within the for...in iteration are physically incapable of completing by the time your next iteration begins and your code is suffering from illegal memory overwrites.

OR

  1. that with 1000 entities, your call to .save creates a massive "hit" to performance and, depending on other processes you have running at the time, especially processes that modify the NSManagedObjectContext, you may simply run out of system resources. I suspect that, depending on "other" processes you have running in your code at the time of the call to .save, you are simply short on system resources and memory overwrites are perhaps a mechanism, albeit failed, as an attempt for iOS to "keep up".

Yeah, I'm covering a lot of ground here but attempting to point you in a direction that will help solve your problem.

Perhaps your question becomes... how do I ensure the Core Data processes within my iteration are complete before stepping and/or saving?

All the books and other information I've read on Core Data suggest that operations within an NSManagedObjectContext are cheap and saves are expensive.

The following is a direct quote from a book, which I suspect is allowable on this site if I credit the authors...

“It’s important to note, though, that inserting new objects into the context or changing existing objects are both very cheap — these operations only touch the managed object context. Inserts and changes all operate within the context without touching the SQLite tier.

Only when these changes are being saved to the store is the cost relatively high. A save operation involves both the coordinator tier and the SQL tier. As such, a call to save() is rather expensive.

The key takeaway is quite simply to reduce the number of saves. But there’s a balance to strike: saving a very large number of changes increases the memory usage because the context has to keep track of these changes until they’re saved. And processing a large change requires more CPU and memory than a smaller one. However, in total, a large changeset is cheaper than many small ones.”

Excerpt From: “Core Data.” by Florian Kugler and Daniel Eggert. (objc.io)

I don't have a clear answer for you now because frankly this will take some time and effort for me to solve, but in the first instance I recommend that you:

  • read up on performance tuning Core Data;
  • investigate stripping back your call to MyEntity(context:) to the bare essentials - for example, creating an entity with relationship faults or creating an entity and calling into memory only those attributes needed for your code to function;
  • investigate the use of NSOperation or Operation to queue your tasks;
  • watch the WWDC2015 talk on "Advanced NSOperations".

Further SO reading, if you're interested:

andrewbuilder
  • 3,629
  • 2
  • 24
  • 46
  • i upvoted because this is a great analysis andrew. however, since I need an actual solution to the problem, I think it's time i offer some bounty to get the problem solved. my patience has died haha – angryip Aug 30 '19 at 21:34
  • 1
    some other possibilities to think about... build you data into an in-memory structure such as a `dictionary`, using a `Struct`, then populate / modify your `MyEntity`via a separate iteration, using a background thread or child context and implement a closure with a completion... that completion will be the call to `.save`. – andrewbuilder Aug 30 '19 at 23:44
  • I ended up doing just that. while it helped to minimize the problem, it still randomly crashes at times. :/ but definitely getting closer. – angryip Aug 31 '19 at 00:37
0

Try this...

class MySampleClass {

    let dataLogger = DataLogger.shared
    let context = dataLogger.context

    static func doSomething {
        for i in 0 ..< 1000 {
            let entry = MyEntity(context: context);
            // do random things
        }
        dataLogger.save()
    }
}

By instantiating DataLogger Singleton reference once in your class and setting a property to its context, you are holding a local reference that should save repeated calls back to your DataLogger class.

The following is a “side issue” comment - I don’t believe it is a part of your issue - but... I’m not certain you need the call self.context.perform {}. Others are perhaps better informed to explain why. Personally I’ll do some research and update my answer if I figure out why.

andrewbuilder
  • 3,629
  • 2
  • 24
  • 46
  • tried this, but no success. remains to crash per the scenario described in the post. clever idea however. – angryip Aug 29 '19 at 12:50