How to really slow down a Core Data fetch

Core Data is a bit of a mysterious thing. Sometimes, patterns that seem helpful in theory can be disastrous in practice. Consider, for example, the instinct not to save changes to disk, or to do so very infrequently, for fear of slowing down processing or blocking the main thread. This is something I’ve done, and seen others do, in projects using Core Data.

What’s easy to overlook is that unsaved changed in Core Data can make fetch requests slower. Sometimes, orders of magnitude slower. Which could undo all the benefits of deferred saves. Here’s a real-life example with some numbers.

The code below was used retrieve all the Word entities in an object graph whose string attribute was equal to one of the strings in an array, stringsToFetch. There were 28,720 words in the graph, and the fetch matched 2,044 of them.

NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:@"Word"];
fetch.predicate = [NSPredicate predicateWithFormat:@"string IN %@", stringsToFetch];
NSArray *results = [context executeFetchRequest:fetch error:nil];

In one test, the objects had been inserted in the context but save: had not yet been called. In the other, the inserted objects had been saved to disk. (The code was run on an iPad Air.)

Saved objects Unsaved objects Fetch time
0 28,720 4.55 secs
28,720 0 0.07 secs

The performance of the fetch with unsaved changes was, in a word, hideous. This is obviously an extreme case — nearly 30,000 unsaved insertions — but the effect was quite linear. Even 3,000 or so unsaved objects slowed the fetch down to a still-needlessly-long 0.5 seconds.

The reason for the slowdown is clear if you run the same code using the Time Profiler. When the unsaved objects are present, about 70% of the processor time is spent on string comparisons that descend from the call to executeFetchRequest:, in which our predicate is being evaluated. So in essence there are two fetches: One is a super-fast SQL query, and the other is a ponderously slow series of in-memory string comparisons.

Screen Shot 2014-09-12 at 10.54.17 AM

Keep in mind: You won’t uncover this problem by using the “Fetch Duration” data from the Core Data Fetches tool in Instruments. That’s because this tool seems to return the duration of the SQL query only: It doesn’t account for the time that was spent evaluating in-memory objects as well. You need to put a timer around the actual call to executeFetchRequest: to see the true processing time.

Every scenario is different, but I highly suspect that for some people who complain that “Core Data fetching is slow”, the problem isn’t a trip to disk, but the opposite: too many inserted, updated and/or deleted objects in memory.

Additive animations: animateWithDuration in iOS 8

There’s new behavior involving animateWithDuration: in iOS 8 that can help make certain “interruptible” animations a lot smoother.

The classic use case is a togglable animation that can be reversed mid-flight, like a drawer that opens or closes when a button is tapped. The gist of the change is that in iOS 8, when calls to animateWithDuration: overlap, any previously scheduled, in-flight animations on the same properties will no longer be yanked out of the view’s layer, but instead be allowed to finish even as the new animation takes effect and is blended with the old one(s). (For properties that adopt this additive animation behavior, it will happen whether or not you use the UIViewAnimationOptionBeginFromCurrentState option.)

Consider the example of a view whose center.y is being animated from 0 to 100 over 1 second using animateWithDuration:. Halfway though, at the 0.5-second mark, a second animateWithDuration: block, also with a 1-second duration, sends the view back to 0.

In iOS 7 and earlier, using the UIViewAnimationOptionBeginFromCurrentState option and the default animation curve (UIViewAnimationOptionCurveEaseInOut), the complete animation would look like this:

Non-Additive Animation Curve

At 0.5 seconds, when the second animation block is called, a new CABasicAnimation gets added to the animating view’s CALayer with the key position and the keypath position, replacing the previous one still in flight. The starting position for the new animation is animating view’s current position — that is, the position of its layer’s presentationLayer.

The resulting 1.5-second animation is continuous, in the sense that the view does not jump to a new position. But the speed changes abruptly in both magnitude and direction at 0.5 seconds. Not so pretty.

In iOS 8, however, the same sequence produces a very different animation — see the dotted blue line below:

Additive Animation Curve (Ease In, Ease Out)

At 0.5 seconds, a second CABasicAnimation is added, but with a different key than the first one — the system happens to use position-2 — and both animations are allowed to run their course. Because both animations have the additive property set to YES, the position changes are added together. (The red and yellow lines don’t add up to the blue line because the animation values are relative — to the model position — and not absolute; the actual math involves positive and negative values that offset each other.)

The result is a smooth curve that, in this example, peaks at 0.75 seconds, as the animating view overshoots and then reverses itself.

You can continue to add animations in rapid succession using animateWithDuration:, and the layer will accumulate additive animations with keys like position-3, position-4, etc. The visual effect is generally quite smooth and natural.

This new behavior isn’t so pretty, however, for animations using a linear timing function. In this simple example, if the UIViewAnimationOptionCurveLinear option were used instead of the default ease-in-ease-out, the additive animations would cancel eachother out, resulting in the view being “frozen” until the previous animation ended. This definitely looks weird. See the 0.5-second plateau in the blue curve:

Additive Animation Curve, Linear

Since you apparently can’t opt out of additive animations in iOS 8, you’d need to do a bit of extra work to restore the old, non-additive behavior. In the simplest case, you could simply rip out any in-flight animations yourself before the new call to animateWithDuration:, making sure to manually reset the layer’s position to sync up with the presentation layer. Something like this, right before the new animation block, seems to work:

CALayer *presLayer = (CALayer *)self.animatingView.layer.presentationLayer;
self.animatingView.layer.position = [presLayer position];
[self.animatingView.layer removeAllAnimations];

In most cases, though, I assume the additive animations will be welcome as an easy way to smooth out overlapping transitions.

Check out this WWDC 2014 video for more on additive animations in iOS 8.