Ad

Our DNA is written in Swift
Jump

CoreText Loading Performance

Somebody people told me that some function in my NSAttributedStrings+HTML would take forever but whenever I tested it, I could not see anything wrong. Then Stuart Carnie was able to send a snippet of code that, when pasted into appDidFinishLaunching, would exhibit the same problem, duplicatable.

I was stumped at first. How could I have missed it? But at second glance Stuart did not reference any of my classes, but was only using standard SDK calls. Yet, those are almost identical to what I had wrapped into DTCoreTextFontDescriptor, my Objective-C wrapper.

Then it dawned on me: this might be a lazy loading problem. Or maybe even a bug in CoreText.framework.

So I modified Stuart’s snippet to perform the same test several times, with changing font sizes and font families.

- (void)testWithSize:(CGFloat)size family:(NSString *)family
{
    NSLog(@"Start");
    NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
    [attributes setObject:family forKey:(id)kCTFontFamilyNameAttribute];
    [attributes setObject:[NSNumber numberWithFloat:size] forKey:(id)kCTFontSizeAttribute];
    CTFontDescriptorRef fontDesc = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)attributes);
    CTFontRef matchingFont = CTFontCreateWithFontDescriptor(fontDesc, size, NULL);
    CFRelease(matchingFont);
    CFRelease(fontDesc);
    NSLog(@"Finish");
}
 
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [self testWithSize:10 family:@"Courier New"];
    [self testWithSize:20 family:@"Georgia"];
    [self testWithSize:30 family:@"Helvetica"];
// ...
}

On my iPhone 4 the first test took 1284 ms, the second improved dramatically to 39 ms and the last one to only 13 ms. Clearly the very first time you are accessing a CTFontDescriptor function this causes the CoreText dynamic framework to be loaded and initialized.

By extension this probably means that you would see the same problem for ANY CoreText function. Subsequent calls are as fast as you would expect them to be. Though Stuart comes to a slightly different conclusion than me:

I am fairly confident it is not a dynamic lib loading issue,
because the first API call to CoreText (CTFontDescriptorCreateWithAttributes)
is not slow.  When I profiled into core text, it is a lot of code running in
freetype library.

This also explains why I was not seeing the problem myself. Because of the way the demo is structured this loading would occur during the first table view cell of the demo list was drawn. And since this was the first visible view in the demo app this would delay presentation of the UI.

Stuart and me filed bug reports: 9350255 and 9350318 respectively. If you are affected by this problem too, then I encourage you to reference these two Radars in your own bug report.

Now for a workaround Stuart proposes to use a background dispatch queue to force loading of the CoreText.framework without affecting either app startup or first use.

dispatch_queue_t queue = dispatch_queue_create("com.swatch.worker", NULL);
dispatch_async(queue, ^(void) {
    NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
    [attributes setObject:@"Helvetica" forKey:(id)kCTFontFamilyNameAttribute];
    [attributes setObject:[NSNumber numberWithFloat:36.0f] forKey:(id)kCTFontSizeAttribute];
    CTFontDescriptorRef fontDesc = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)attributes);
    CTFontRef matchingFont = CTFontCreateWithFontDescriptor(fontDesc, 36.0f, NULL);
    CFRelease(matchingFont);
    CFRelease(fontDesc);
});
dispatch_release(queue);

This kind of collaboration with talented people like Stuart is exactly why I OpenSourced this project. Thanks to Stuart we were able to corner the problem!

It’s always great to know that it’s not your code that is the issue, but a performance problem in a public framework is to blame.


Categories: Administrative

4 Comments »

  1. Regarding why you do not see it in your App is exactly correct. I actually tested yours first, and it was fine, but if you disable access to the tableview cells content view (so they are all blank), you will see the problem when you tap on the first cell.

  2. Feels good to hear that you are right. 🙂 You are right, too. Very much so. 🙂

  3. A little birdie told me this is probably fixed in iOS5, so hold off on any new bug reports til you test with iOS 5.