Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Performance of NSManagedObjectContext save degrades dramatically

I am having issues with a CoreData-based iOS app when it tries to build the initial DB from data sent from the server. Basically, the server sends down 1MB chunks of objects (about 3,000 per chunk), and the iOS client deserializes them and writes them into disk.

What I'm seeing is that everything is going pretty well for about the first 8 chunks (out of 44), then performance drops off dramatically and each chunk starts taking longer and longer, as in the image below. Pretty much all the time is consumed in [NSManagedObjectContext save] as you can see in the Instruments profiling data, but also it appears that the app is no longer running at 100% of CPU for some reason, like it's waiting on disk I/O or something.

profiling data showing performance degradation

A few important facts about how I'm doing this:

  • Each chunk is processed in its own NSManagedObjectContext with its own NSAutoreleasePool, so there is no object build-up in a non-flushed context between processing of chunks.

  • There is no NSUndoManager set on any of the contexts.

  • There is no mergeChangesFromContextDidSaveNotification: going on (i.e. the chunk contexts aren't pushing their changes into a "master" context)

  • I'm using a SQLite-based datastore on iOS 4.3.

  • The records being written do have indexes on them.

  • The entire sync job is processed on a single GCD background thread (i.e. dispatch_queue_create() and dispatch_async()).

I have no idea why the performance suddenly drops off like that or what can be done to address it. I have poked around and read the following, but nothing has jumped out at me yet:

  • http://cocoawithlove.com/2008/03/testing-core-data-with-very-big.html

  • Does the performance of saving a ManagedObjectContext depend on the number of contained (unchanged) objects?

Any ideas or pointers for making this app scale up to 100,000 records in the database would be much appreciated.

Edit - extra stats

This Instruments graph shows the same simulation as above (on iPad2), but includes the disk activity stats and you can see pretty plainly that all of the "not running at 100% CPU" time seems to be taken up with writing to disk.

Disk activity for original test

I also ran same sync attempt running on the iOS simulator. Overall memory usage is more or less constant for each chunk except for a dictionary that contains object IDs that grows slightly over time (but these are not CoreData objects or anything that would affect saves, they are just NSNumbers). This dict is a small amount of memory compared to the total heap and so the problem is not running out of memory.

What is interesting about this test is that the CoreData Save instrument reports that the successive saves take roughly the same amount of time, which obviously conflicts with the CPU profiling information from the first set of results. It seems like CoreData thinks it is taking the same amount of time to push changes to the DB, but the DB itself (i.e. SQLite) suddenly takes a lot longer to actually stream those changes to disk.

like image 541
glenc Avatar asked Jun 16 '11 15:06

glenc


People also ask

What is NSManagedObjectContext?

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

How do I save an object in CoreData?

To save an object with Core Data, you can simply create a new instance of the NSManagedObject subclass and save the managed context. In the code above, we've created a new Person instance and saved it locally using Core Data.


2 Answers

I know this is an old issue, so this is probably no longer relevant for you, but it may be to someone else.

I've seen performance issues seeding a Core Data database over iCloud and discovered that if you have inverse relationships on the data model you can be hurt incredibly badly performance wise. The way iCloud transaction logging has been implemented, it actually seems to be an inevitable problem. Each transaction sent to iCloud (have a look at them on developer.icloud.com - they're just zipped up plists) records every relationship that is affected by a change. Unlike when you modify one end of an relationship in Core Data, and it takes care of the inverse end, the core data transaction log ends recording the changes at BOTH ends, rather than it working it out.

So if you have a 1 to many relationship, and you create another record which will end up hanging off the 'many' end - well the record at the '1' end will also be updated to reflect the fact a new additional record is now hanging off it. If you have an architecture that means you have a 'type' object that lots of 'data' objects hang off, then every time you add a new data object, the type one is going to have a transaction written for it as well - but here's the kicker, because the iCloud Core Data transactions record the ENTIRE state of edited entities, not just the changes, EVERY relationship already recorded against it is also added to the log, not just the one indicating the new subordinate record. This can quickly spiral out of control as the amount of data written grows as the number of relationships between entities grows - it ends up taking longer and longer to save batches.

I've answered a question a bit like this before here on the Apple dev forums which might be useful as I never seem to be able to describe this succinctly.

The easiest option to improve seeding performance if this scenario is what is impacting you is to switch inverse relationships off, but this isn't always an option.

like image 161
Rob Glassey Avatar answered Oct 23 '22 10:10

Rob Glassey


More information about your implementation would help. For example, do you run this on the main thread or are you implementing background threads? However, I have seen this behavior before. When performing extensive batch operations using Core Data, it can slow down if not memory managed properly. Have you checked memory usage? Have you checked for leaks? Another thing to try is to make sure you are using NSAutoreleasePool correctly if needed. By draining the pool periodically, that may help performance.

like image 23
jschmidt Avatar answered Oct 23 '22 11:10

jschmidt