Skip to content
Robbie Hanson edited this page Sep 9, 2013 · 16 revisions

It's easy to achieve great performance with YapDatabase. You just need to understand the basics behind connections and transactions.

Connections & Concurrency

With YapDatabase you can create multiple connections. Each connection is thread-safe. But...

It is important to understand that thread-safe != concurrency. Concurrency comes through using multiple connections.

For example, consider the following bad code:

@implementation MyUITableViewController {
    YapDatabaseConnection *databaseConnection;
}

// ...
- (UITableViewCell *)tableView:(UITableView *)sender cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    __block BNBook *book = nil;
    [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {    
        book = [[transaction ext:@"order"] objectAtIndex:indexPath.row inGroup:@"books"];
    }];
    
    // create and configure cell using book...
    return cell;
}

- (void)missingBooksDidDownload:(NSArray *)books
{
    // BAD CODE!
    [databaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        
        for (BNBook *book in books) {
            [transaction setObject:book forKey:book.uuid];
        }
    }];
}

Why is this bad code? I'm doing an async operation to avoid disk IO on the main thread. What's the problem?

The above code will technically work, but the performance won't be what you want.

Every YapDatabaseConnection only supports a single transaction at a time.

Essentially, each YapDatabaseConnection has an internal serial queue. All transactions on the connection must go through the serial queue. This includes both read-only transactions and read-write transactions. It also includes async transactions.

Thus, in the bad code example above, we are blocking the main thread by blocking the single databaseConnection. That is, we are doing an "expensive" (disk IO) read-write transaction, which will block the 'databaseConnection' until it completes.

A read-write transaction on connectionA will block read-only transactions on connectionA until the read-write transaction completes.

To achieve concurrency we use multiple connections. Here's the good code:

@implementation MyUITableViewController {
    YapDatabaseConnection *uiConnection;
    YapDatabaseConnection *bgConnection;
}

// ...
- (UITableViewCell *)tableView:(UITableView *)sender cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    __block BNBook *book = nil;
    [uiConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {    
        book = [[transaction ext:@"order"] objectAtIndex:indexPath.row inGroup:@"books"];
    }];
    
    // create and configure cell using book...
    return cell;
}

- (void)missingBooksDidDownload:(NSArray *)books
{
    // GOOD CODE.
    [bgConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        
        for (BNBook *book in books) {
            [transaction setObject:book forKey:book.uuid];
        }
    }];
}

Connections & Cost

You can achieve concurrency by using multiple connections. And it's just so darn easy to create a connection that it becomes easy to forgot that connections aren't free.

Here's some more bad code:

- (UITableViewCell *)tableView:(UITableView *)sender cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // HORRIBLE CODE!
    __block BNBook *book = nil;
    [[database newConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) {    
        book = [[transaction ext:@"order"] objectAtIndex:indexPath.row inGroup:@"books"];
    }];
    
    // create and configure cell using book...
    return cell;
}

- (void)missingBooksDidDownload:(NSArray *)books
{
    // BAD CODE!
    [[database newConnection] asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        // ...
    }];
}

- (void)didDownloadUpdatedBook:(BNBook *)book
{
    // BAD CODE!
    [[database newConnection] asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        // ...
    }];
}

- (void)didUpdateTop20List:(NSArray *)books
{
    // BAD CODE!
    [[database newConnection] asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        // ...
    }];
}

You should consider connections to be relatively heavy weight objects.

OK, truth be told they're not really that heavy weight. I'm just trying to scare you. Because in terms of performance, you get a lot of bang for your buck if you recycle your connections.

Many of the performance optimizations within the YapDatabase architecture are within connections. But you only get these optimizations if your connection sticks around for awhile.

One example of this is the cache. Each YapDatabaseConnection maintains its own cache. So if you have a connection that sticks around for awhile, it will automatically start populating its cache with the objects you're accessing. This is particularly beneficial for situations like fetching objects for a UITableView. Because it means that scrolling around in a UITableView is going to be hitting the cache almost the entire time. And hitting the cache is fast. Really really fast.

Another example is pre-compiled sqlite statements. In order to execute a statement in SQL, the sqlite engine has to parse the SQL text statement into a byte-code program. This is called compiling the SQL statement. And the resulting "compiled statement" is explicitly tied to a single sqlite connection. So the very first time you call setObject:forKey: the YapDatabaseConnection has to ask sqlite to compile the routine. The second time you call setObject:forKey: the connection can recycle the pre-compiled statement. Thus subsequent transactions on the same connection skip this overhead and inherit a little performance boost.

Additionally, maintaining a persistent connection allows you to configure it to match your needs.

Here's the good code:

@implementation MyTableViewController {
    YapDatabaseConnection *uiConnection;
    YapDatabaseConnection *bgConnection;
}

- (void)viewDidLoad
{
    uiConnection = [[AppDelegate database] newConnection];
    bgConnection = [[AppDelegate database] newConnection];
    
    uiConnection.objectCacheLimit = 500; // increase object cache size
    uiConnection.metadataCacheEnabled = NO; // not using metadata on this connection

    bgConnection.objectCacheEnabled = 500; // don't need cache for write-only connection
    bcConnection.metadataCacheEnabled = NO; // don't need cache for write-only connection
}

- (UITableViewCell *)tableView:(UITableView *)sender cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Good code.
    __block BNBook *book = nil;
    [uiConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {    
        book = [[transaction ext:@"order"] objectAtIndex:indexPath.row inGroup:@"books"];
    }];
    
    // create and configure cell using book...
    return cell;
}

- (void)missingBooksDidDownload:(NSArray *)books
{
    // Good code.
    [bgConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        // ...
    }];
}

- (void)didDownloadUpdatedBook:(BNBook *)book
{
    // Good code.
    [bgConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        // ...
    }];
}

- (void)didUpdateTop20List:(NSArray *)books
{
    // Good code.
    [bgConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        // ...
    }];
}

Read-Only vs Read-Write Transactions

Clone this wiki locally