Dealing with Slow Security Scoped Bookmarks

As I mentioned in a previous post I found security scoped bookmarks to be up to 220 times slower than regular bookmarks. Since my application was already using security scoped bookmarks to track many resources this caused major performance regressions after sandboxing my app and switching over to security bookmarks.

To fix this I ended up doing URL resolution on a background thread so that the app stays responsive while taking up to 20 seconds to resolve bookmarks.

I decided to use an NSOperationQueue to manage the operations. So I created a singleton queue to be used exclusively for resolving bookmarks like this:

+ (NSOperationQueue *)bookmarkLoadingQueue {
    static NSOperationQueue *gBookmarkLoadingQueue = nil;
    if (gBookmarkLoadingQueue == nil) {
        gBookmarkLoadingQueue = [[NSOperationQueue alloc] init];
        [gBookmarkLoadingQueue setMaxConcurrentOperationCount:3];
        [gBookmarkLoadingQueue setName:@"Bookmark Loading Queue"];
    }
    return gBookmarkLoadingQueue;
}

The most interesting part here is that I found it necessary to set the max number of concurrent operations on the queue. I believe that an NSOperationQueue is supposed to manage this for you based on the system you are running on, but it turns out that it is very bad at this if the operations you are adding are resolving security scoped bookmarks. I found that adding more than 80 operations to the queue hung the entire app. Update: Mike Abdullah pointed out that the reason this occurs is because NSOperationQueue can only effectively manage the number of operations if they are CPU-bound and these operations are not CPU-bound.

I ended up settling on 3 after testing various queue widths and finding that this setting provided the fastest results (some concurrency but not too much). Of course the optimal result would be different on different machines, but I thought this number was good enough.

Once I got the queue set up I added each bookmark resolution as a single operation on the queue:

for (NSData *securityBookmark in [self securityBookmarks]) {
    [[AppDelegate bookmarkLoadingQueue] addOperationWithBlock:^{
        NSURL *url = [NSURL URLByResolvingBookmarkData:securityBookmark options:(NSURLBookmarkResolutionWithSecurityScope|NSURLBookmarkResolutionWithoutUI|NSURLBookmarkResolutionWithoutMounting) relativeToURL:nil bookmarkDataIsStale:nil error:nil];
        if (url == nil) {
            NSLog(@"Error resolving security bookmark!!!\n");
            return;
        }
        // do something with the url
    }];
}

In my case I also wanted to know when all the bookmarks had been resolved so that I could do some work on the main thread so I just created an operation that was dependent on all the resolution operations.

BookmarkOperation *finishedOperation = [BookmarkOperation blockOperationWithBlock:^{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // do work now that we have all the urls
    }];
}];

I found this approach to be 3 times faster than performing the operations on a single thread and since the work occurs on the background thread my application stays responsive even when resolving hundreds of bookmarks.

 
5
Kudos
 
5
Kudos

Now read this

Sandboxing Adventures

I recently had the pleasure of sandboxing a Mac application that was written before sandboxing was required on the Mac App Store. I found a lot of good resources (including Apple’s Documentation) that covered the basics of setting up... Continue →