Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Data multi-NSManagedObjectContext performance puzzle

I have an iOS Core Data performance problem that I have not been able quite to figure out. My app has a relatively straightforward UICollectionViewController with a function to create an NSFetchedResultsController roughly like the following:

func loadCollection(collectionID: String) {
    let fetchRequest = NSFetchRequest(entityName: "Story")
    fetchRequest.predicate = NSPredicate(format: "collectionID == %@ AND wasDeleted == FALSE", collectionID)

    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "yearMonthDay", ascending: true), NSSortDescriptor(key: "startDate", ascending: true)]
    fetchRequest.fetchBatchSize = 100

    self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: mainObjectContext!,
                                                                sectionNameKeyPath: "yearMonth", cacheName: collectionID)
    var error: NSError?
    self.fetchedResultsController.performFetch(&error)
    ....
}

My Core Data stack looks roughly like the following with two managed object contexts to improve the UI response time for object saves for new and edited "Story" objects. This is the basic pattern I've seen in places like Marcus Zarra's Core Data book and blog post.

// Variation "A" with two managed object contexts.
lazy var mainObjectContext: NSManagedObjectContext? = {

    if let coordinator = self.persistentStoreCoordinator {

        self.rootContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
        self.rootContext?.persistentStoreCoordinator = coordinator

        let managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
        managedObjectContext.parentContext = self.rootContext
        return managedObjectContext  

    } else {
        return nil
    }
}()

The problem is that when I have two contexts like above, the initial fetchedResultsController.performFetch() above takes about 10x longer than it does when I have only a single (.MainQueueConcurrencyType) context like below.

For example, performing the fetch above with two contexts and with about 1,700 objects takes over 5 seconds on an iPad Air, but less than 0.4 seconds with just one context like below.

// Variation "B" with one managed object context.
    lazy var mainObjectContext: NSManagedObjectContext? = {

        if let coordinator = self.persistentStoreCoordinator {

            let managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
            managedObjectContext.persistentStoreCoordinator = coordinator
            return managedObjectContext  

        } else {
            return nil
        }
    }()

I turned on Core Data logging (-com.apple.CoreData.SQLDebug 1) and from the logging I can see that when I have two object contexts, all of the properties in 1,700 objects are being loaded from the SQL database for the initial fetch. But if I only have one context, no properties are being initially loaded from the database.

    Log from variation "A" with two object contexts:
    2015-08-18 13:31:49.761 Myapp[27646:4374573] CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZAUTOCREATED, t0.ZBLOCKS, t0.ZCHANGETOKEN, t0.ZCOLLECTIONID, t0.ZCREATIONDATE, t0.ZEDITDATE, t0.ZENDDATE, t0.ZFULLTEXT, t0.ZGLOBALID, t0.ZLOCATION, t0.ZLOCATIONTEXT, t0.ZSTARTDATE, t0.ZTAGS, t0.ZTHUMBNAILIMAGE, t0.ZTIMEZONE, t0.ZTITLE, t0.ZWASDELETED, t0.ZYEARMONTH, t0.ZYEARMONTHDAY FROM ZSTORY t0 WHERE ( t0.ZCOLLECTIONID = ? AND  t0.ZWASDELETED = ?) ORDER BY t0.ZYEARMONTHDAY, t0.ZSTARTDATE
    2015-08-18 13:31:52.093 Myapp[27646:4374573] CoreData: annotation: sql connection fetch time: 2.3314s
    2015-08-18 13:31:55.011 Myapp[27646:4374573] CoreData: annotation: total fetch execution time: 5.2502s for 1743 rows.
    2015-08-18 13:31:55.016 Myapp[27646:4374573] CoreData: sql: SELECT t0.ZYEARMONTH, COUNT (DISTINCT  t0.Z_PK) FROM ZSTORY t0 WHERE ( t0.ZCOLLECTIONID = ? AND  t0.ZWASDELETED = ?) GROUP BY  t0.ZYEARMONTH ORDER BY t0.ZYEARMONTH
    2015-08-18 13:31:55.276 Myapp[27646:4374573] CoreData: annotation: sql connection fetch time: 0.2590s
    2015-08-18 13:31:55.276 Myapp[27646:4374573] CoreData: annotation: total fetch execution time: 0.2600s for 371 rows.


    Log from variation "B" with one object context:
    2015-08-18 13:25:19.448 Myapp[27635:4362501] CoreData: sql: SELECT 0, t0.Z_PK FROM ZSTORY t0 WHERE ( t0.ZCOLLECTIONID = ? AND  t0.ZWASDELETED = ?) ORDER BY t0.ZYEARMONTHDAY, t0.ZSTARTDATE
    2015-08-18 13:25:19.789 Myapp[27635:4362501] CoreData: annotation: sql connection fetch time: 0.3405s
    2015-08-18 13:25:19.789 Myapp[27635:4362501] CoreData: annotation: total fetch execution time: 0.3410s for 1743 rows.
    2015-08-18 13:25:19.790 Myapp[27635:4362501] CoreData: sql: SELECT t0.ZYEARMONTH, COUNT (DISTINCT  t0.Z_PK) FROM ZSTORY t0 WHERE ( t0.ZCOLLECTIONID = ? AND  t0.ZWASDELETED = ?) GROUP BY  t0.ZYEARMONTH ORDER BY t0.ZYEARMONTH
    2015-08-18 13:25:19.825 Myapp[27635:4362501] CoreData: annotation: sql connection fetch time: 0.0339s
    2015-08-18 13:25:19.825 Myapp[27635:4362501] CoreData: annotation: total fetch execution time: 0.0349s for 371 rows.

So based on those results, I modified the loadCollection function to add

fetchRequest.includesPropertyValues = false 

Now with two object contexts, I get the following, with the initial load time back under 0.4 seconds - excellent!:

    2015-08-18 13:53:43.345 Myapp[27692:4414951] CoreData: sql: SELECT 0, t0.Z_PK FROM ZSTORY t0 WHERE ( t0.ZCOLLECTIONID = ? AND  t0.ZWASDELETED = ?) ORDER BY t0.ZYEARMONTHDAY, t0.ZSTARTDATE

    2015-08-18 13:53:43.683 Myapp[27692:4414951] CoreData: annotation: sql connection fetch time: 0.3378s

    2015-08-18 13:53:43.684 Myapp[27692:4414951] CoreData: annotation: total fetch execution time: 0.3383s for 1743 rows.

    2015-08-18 13:53:43.688 Myapp[27692:4414951] CoreData: sql: SELECT t0.ZYEARMONTH, COUNT (DISTINCT  t0.Z_PK) FROM ZSTORY t0 WHERE ( t0.ZCOLLECTIONID = ? AND  t0.ZWASDELETED = ?) GROUP BY  t0.ZYEARMONTH ORDER BY t0.ZYEARMONTH

    2015-08-18 13:53:43.722 Myapp[27692:4414951] CoreData: annotation: sql connection fetch time: 0.0329s

    2015-08-18 13:53:43.722 Myapp[27692:4414951] CoreData: annotation: total fetch execution time: 0.0339s for 371 rows.

BUT, now every object fetched for the NSFetchedResultsController is a fault and results in a one row SQL query, which noticably slows collection view scrolling:

2015-08-18 13:53:44.078 Myapp[27692:4414951] CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZAUTOCREATED, t0.ZBLOCKS, t0.ZCHANGETOKEN, t0.ZCOLLECTIONID, t0.ZCREATIONDATE, t0.ZEDITDATE, t0.ZENDDATE, t0.ZFULLTEXT, t0.ZGLOBALID, t0.ZLOCATION, t0.ZLOCATIONTEXT, t0.ZSTARTDATE, t0.ZTAGS, t0.ZTHUMBNAILIMAGE, t0.ZTIMEZONE, t0.ZTITLE, t0.ZWASDELETED, t0.ZYEARMONTH, t0.ZYEARMONTHDAY FROM ZSTORY t0 WHERE  t0.Z_PK = ? 

2015-08-18 13:53:44.081 Myapp[27692:4414951] CoreData: annotation: sql connection fetch time: 0.0011s

2015-08-18 13:53:44.081 Myapp[27692:4414951] CoreData: annotation: total fetch execution time: 0.0029s for 1 rows.

2015-08-18 13:53:44.082 Myapp[27692:4414951] CoreData: annotation: fault fulfilled from database for : 0xd00000001ad8000c <x-coredata://967F5940-D73C-48E2-A26D-E60FF87BA9F7/Story/p1718>

Instead of batches of 100 or so like this with the single context and includesPropertyValues defaulted to true:

2015-08-18 14:47:44.221 Myapp[27792:4515252] CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZAUTOCREATED, t0.ZBLOCKS, t0.ZCHANGETOKEN, t0.ZCOLLECTIONID, t0.ZCREATIONDATE, t0.ZEDITDATE, t0.ZENDDATE, t0.ZFULLTEXT, t0.ZGLOBALID, t0.ZLOCATION, t0.ZLOCATIONTEXT, t0.ZSTARTDATE, t0.ZTAGS, t0.ZTHUMBNAILIMAGE, t0.ZTIMEZONE, t0.ZTITLE, t0.ZWASDELETED, t0.ZYEARMONTH, t0.ZYEARMONTHDAY FROM ZSTORY t0 WHERE  t0.Z_PK IN (SELECT * FROM _Z_intarray0)  ORDER BY t0.ZYEARMONTHDAY, t0.ZSTARTDATE LIMIT 100
2015-08-18 14:47:44.468 Myapp[27792:4515252] CoreData: annotation: sql connection fetch time: 0.0959s
2015-08-18 14:47:44.469 Myapp[27792:4515252] CoreData: annotation: total fetch execution time: 0.2566s for 100 rows.
2015-08-18 14:47:44.550 Myapp[27792:4515252] CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZAUTOCREATED, t0.ZBLOCKS, t0.ZCHANGETOKEN, t0.ZCOLLECTIONID, t0.ZCREATIONDATE, t0.ZEDITDATE, t0.ZENDDATE, t0.ZFULLTEXT, t0.ZGLOBALID, t0.ZLOCATION, t0.ZLOCATIONTEXT, t0.ZSTARTDATE, t0.ZTAGS, t0.ZTHUMBNAILIMAGE, t0.ZTIMEZONE, t0.ZTITLE, t0.ZWASDELETED, t0.ZYEARMONTH, t0.ZYEARMONTHDAY FROM ZSTORY t0 WHERE  t0.Z_PK IN  (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)  ORDER BY t0.ZYEARMONTHDAY, t0.ZSTARTDATE LIMIT 100
2015-08-18 14:47:44.614 Myapp[27792:4515252] CoreData: annotation: sql connection fetch time: 0.0067s
2015-08-18 14:47:44.614 Myapp[27792:4515252] CoreData: annotation: total fetch execution time: 0.0635s for 44 rows.

Any thoughts or suggestions on a better way to do this will be most appreciated.

like image 266
LenK Avatar asked Aug 18 '15 22:08

LenK


People also ask

What is NSManagedObjectContext in Core Data?

An object space to manipulate and track changes to managed objects.

Can we have multiple managed object context in Core Data?

Most apps need just a single managed object context. The default configuration in most Core Data apps is a single managed object context associated with the main queue. Multiple managed object contexts make your apps harder to debug; it's not something you'd use in every app, in every situation.

What is the purpose of NSManagedObjectContext?

A managed object context is an instance of NSManagedObjectContext . Its primary responsibility is to manage a collection of managed objects. These managed objects represent an internally consistent view of one or more persistent stores.

Is Managedobjectcontext thread safe?

Managed Object ContextThe NSManagedObjectContext class isn't thread safe. Plain and simple. You should never share managed object contexts between threads. This is a hard rule you shouldn't break.


1 Answers

It is a known issue with NSFetchedResultsController and Batch Size. You cannot use Parent-Child contexts without losing the batchSize feature. See this answer for details.

In other words, If you need to use NSFetchedResultsController and need best performance you must attach it to a main thread context and that context must be attached to a persistent store coordinator.

like image 130
Pablo Romeu Avatar answered Oct 26 '22 00:10

Pablo Romeu