Debugging Core Data

Apple recommends adding some launch arguments and environment variables to your Xcode schemes to catch and debug Core Data problems. I’ve known about some of these for a long time others were new to me.

Quick summary:

Apple’s recommendations are buried in a WWDC video so here’s a quick summary:

Add two launch arguments:

  • -com.apple.CoreData.SQLDebug 1
  • -com.apple.CoreData.ConcurrencyDebug 1

Leave the SQLDebug argument disabled until needed

Add three environment variables:

  • SQLITE_ENABLE_THREAD_ASSERTIONS 1
  • SQLITE_AUTO_TRACE 1
  • SQLITE_ENABLE_FILE_ASSERTIONS 1

Leave the SQLITE_AUTO_TRACE variable disabled until needed

Before I look at what each argument and variable does let’s recap how to add them to your Xcode project schemes:

Xcode Scheme for Core Data

To configure a scheme, select it in the Xcode toolbar and then choose Edit Scheme from the scheme menu in the Xcode toolbar (⌘<). Select the run action in the left column and switch to the Arguments tab. You can then use the + buttons to add launch arguments and environment variables:

Xcode scheme editor - run phase

Note that the SQLDebug launch argument and the SQLITE_AUTO_TRACE environment variable are not enabled by default.

Check you have them in the test action in the scheme. By default, the test action uses the arguments and environment of the run action. If you’ve customised the test action you’ll need to add them again:

Xcode scheme editor - test phase

If you’re using Xcode test plans you can add the launch arguments and environment variables to a configuration. I’ve added them to the shared settings so they apply to all test runs:

Test plan shared settings

Let’s look at what each of these does:

-com.apple.CoreData.SQLDebug 1

This launch argument is probably already familiar to many Core Data users. It’s been around a long time. It was also the subject of the first post on this site way back in 2010. See Debugging core data on the iPhone.

If you enable this argument in the scheme editor the Core Data framework logs the sql statements it’s using to the console:

CoreData: sql: SELECT 0, t0.Z_PK FROM ZMANAGEDCOUNTRY t0 
ORDER BY t0.ZNAME
CoreData: annotation: sql connection fetch time: 0.0001s
CoreData: annotation: total fetch execution time: 0.0002s for 250 rows.
CoreData: annotation: Bound intarray _Z_intarray0
CoreData: annotation: Bound intarray values.

You can increase the debug level up to level 4. Set it to at least level 3 to see the data returned by a query. This can create a lot of debug output. Apple recommends leaving this argument disabled when you add it to your scheme. You can then quickly enable it when needed and turn it off again afterwards.

-com.apple.CoreData.ConcurrencyDebug 1

This is another old but useful Core Data option. Enabling this launch argument causes your App (or unit test) to throw an exception if you mistakenly access a managed object on the wrong queue. Every NSManagedObject has a managed object context which you must only access on the dispatch queue associated with the context. Get it wrong and bad things can happen. Enabling this arguments helps you discover the mistake during development and testing.

Apple recommends keeping this launch argument enabled in your scheme. You’ll see a single line output to the Xcode console when launching:

CoreData: annotation: Core Data multi-threading assertions enabled.

Now anytime I make the mistake of mixing up my contexts my app will crash. For example, here I’m performing an operation on a background context which uses a private queue. Unfortunately, I’ve made the easy mistake of creating my managed objects on the viewContext which uses the main queue:

let context = dataContainer.newBackgroundContext()
context.performAndWait {
  for country in countries {
    _ = country.toManagedObject(in: dataContainer.viewContext)
  }
  
  try? context.save()
}

Running this code with the ConcurrencyDebug argument enabled triggers an exception:

Xcode exception

I highly recommend keeping this argument enabled during development.

SQLITE Environment Variables

The three environment variables apply to the underlying SQLite engine. Apple recommends keeping the thread and file assertions enabled. I’ve not been able to trigger either so I can’t say how useful they might be.

The SQLITE_AUTO_TRACE variable is disabled by default. When enabled, it’s very similar to the Core Data SQLDebug argument logging the low level SQL operations:

TraceSQL(0x12b60eb10): SELECT 0, t0.Z_PK FROM ZMANAGEDCOUNTRY t0 
ORDER BY t0.ZNAME
TraceSQL(0x12b60eb10): CREATE VIRTUAL TABLE temp.'_Z_intarray0' 
USING '_Z_intarray0'
TraceSQL(0x12b60eb10): SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZAREA, t0.ZCAPITAL,
t0.ZCONTINENT, t0.ZCURRENCY, t0.ZEAST, t0.ZID, t0.ZNAME, t0.ZNORTH,
t0.ZPOPULATION, t0.ZSOUTH, t0.ZVISITED, t0.ZWEST FROM ZMANAGEDCOUNTRY 
t0 WHERE  t0.Z_PK IN (SELECT * FROM _Z_intarray0)   LIMIT 20

Learn More