Core Data Concurrency & Maintaining a Silky Smooth UI
In July 2014, Soundwave introduced a new messaging feature that enabled music lovers to chat and share music with each other. Along with tracking your music listening history and seeing what songs your friends are listening to, users were now able to set up private groups for chatting about music with their friends. This made it even easier to tell your friends about your new favourite tune or to chat with other random Soundwave users who enjoy the same music as you.
From a technical perspective the Soundwave Engineering team decided to use PubNub to provide the realtime messaging. This worked great but in order to have all your messages instantly available we needed to cache the chat messages on the device. On iOS, we used Core Data to do this. This provided a technical challenge because of the potentially large number of chat messages that needed to be fetched and stored as soon as the app opened.
For performance purposes there was a limit of 50 chat groups a user could be a member of. We also fetched messages in blocks of 100, therefore we needed to be able to cater for fetching and storing up to 5000 message in a matter of seconds.
The messaging UI in Soundwave is supported by an NSFetchedResultsController. This ties the list of chat messages directly to Core Data so that any changes in the dataset are automatically updated in the UI. Each chat message that is sent to and from the device is persisted by Core Data and it shows up instantly as a new chat message.
Configuring Core Data
To handle the processing of chat messages with Core Data, the initial Core Data set up consisted of two or more ManagedObjectContext (MOC) objects. The main context was connected to the persistent store and therefore responsible for writing all the chat messages to disk. Each NSFetchedResultsController also used this context, therefore any changes made on this context were immediately reflected in the UI.
The other MOC is a temporary worker context with responsibility for processing large chunks of data on a background queue. It’s relatively cheap to create a context so multiple temporary worker contexts were created when loading and saving chat messages, one for each chat. On each worker context the Main MOC was set as its parent. This meant that each time a worker context is saved, the Main MOC is informed of these changes and therefore caused the UI to be updated. However, this does not persist the data to disk. To do this, an additional saveContext: call is required on the Main MOC.
This worked pretty well for a small amount of data, however during load testing this technique highlighted some concurrency issues. Due to the fact that the Main MOC was also connected to the NSPersistentStoreCoordinator, the UI thread was being blocked as large amounts of data were written to disk. This resulted in a degraded experience for users as the app froze for a number of seconds when data was being saved.
Decoupling the Persistent Store Coordinator
Solving this problem required removing the responsibility of saving to disk from the Main MOC. To do this, we created a brand new MOC whose sole responsibility was to write data to disk. The new configuration can be seen in the diagram below.
The Master Context is tasked with performing the heavy lifting of saving to disk, away from the UI thread. The following code shows the new configuration for each MOC.
As each temporary worker context completes its task, it calls save and this data is automatically reflected in the UI because the main context is set as the worker’s parent. In order to persist the data, the main context executes a save using the performBlock function of Core Data so that data is saved asynchronously on that context. The data on the main context then propagates up to its parent context. If there are no errors as part of this save process, the main context tells the master context to save its data and persist it to disk.
This approach allows all data to flow through the main context and be instantly available on the UI. From a coding perspective, it removes the necessity to listen for Core Data change notifications and to manually merge data from one context to the next. Delegating the persistence of data to its own context on a background thread allows for a silky smooth UI, even when Core Data is processing large amounts of data. Where previously the user was prevented from doing anything on the app while we loaded all the chat messages, we now had a UI that allowed a user to carry on using the app as normal as all the Core Data heavy lifting carried on in the background.
Lead iOS Developer, Soundwave