FYI: View performance improvements (iOS)

199 views
Skip to first unread message

Jens Alfke

unread,
Jul 15, 2014, 9:41:36 PM7/15/14
to mobile-c...@googlegroups.com
I just pushed a couple of changes to the master branch of couchbase-lite-ios that should help improve view indexing performance.

Indexing views together: CBL can now update multiple views’ indexes at the same time, making one pass through the updated documents in the database. That way they share the overhead of reading and parsing the docs. I did some quick benchmarking and found it was about 25% faster to index two views together vs. sequentially.
Currently, the way grouping works is that all views that share a common prefix ending in a “/“ will be indexed together. So “blog/posts” and “blog/comments” will be indexed together, and “profiles/byname” and “profiles/byid” will be indexed together, while “someview” and “otherview” will each index on their own. (This is based on the behavior of CouchDB, where views in the same ‘design document’ index together.)
A good heuristic is that views that are regularly used in the same part of your app’s UI are good to index together, because they’re likely to be queried at around the same time, so you can give them the same name prefix. By giving other views that aren’t used then a different (or no) prefix, you’ll avoid the overhead of re-indexing those when it’s not necessary.

Avoiding the overhead of emitting ‘doc’ as a value: For some reason there’s a lot of map/reduce example code out there (mostly from CouchDB) that emits the entire document (‘doc’) as the value, e.g. it does something like “emit(doc.name, doc)”. In some cases this could speed up queries, but it really slows down indexing because of the unnecessary extra data being stored in the index. (It also slows down queries if your code isn’t going to use all the properties of the document.) The best practice is to emit only the values your query will need, or nil if you don’t need anything.
Since people unfortunately keep doing this in real apps, I’ve added a workaround to make it faster: if the emit() function sees that the value parameter is identical to the doc (by pointer comparison), it instead writes a tiny placeholder into the index. The query code recognizes this placeholder and, when you access CBLQueryRow.value, fetches the document from the database and returns its properties.
I still don’t recommend emitting the doc! You can get the same behavior by emitting nil as the value and just accessing CBLQueryRow.document instead, and it’ll probably be slightly faster. This is just a workaround for an anti-pattern that doesn’t seem to be going away. :)

As I said at the top, these changes were just checked into the master branch, so they probably won’t affect most of you for a while. I do encourage the braver among you to try them out, though, at least to see if they speed up your app. (You might need to rename some views, though.) And we’ll get these ported over to Java and C# too.

—Jens

Len Kawell

unread,
Jul 16, 2014, 1:28:02 PM7/16/14
to mobile-c...@googlegroups.com
It's very helpful to know about not emitting doc. Assuming that I change to emitting nil, how does the efficiency of CBLQuery.prefetch = YES  plus CBLQueryRow.documentProperties compare with CBLQueryRow.document?

Jens Alfke

unread,
Jul 16, 2014, 2:37:20 PM7/16/14
to mobile-c...@googlegroups.com

On Jul 16, 2014, at 10:28 AM, Len Kawell <len.k...@gmail.com> wrote:

It's very helpful to know about not emitting doc. Assuming that I change to emitting nil, how does the efficiency of CBLQuery.prefetch = YES  plus CBLQueryRow.documentProperties compare with CBLQueryRow.document?

Pretty much identical. The .document accessor will use the documentProperties from the prefetch (if available) to populate the CBLDocument, so there won’t be a second db lookup.

There could be a difference if you only end up using some of the query rows’ documents. In that case it could be more efficient to turn off prefetch and just use .document, because it’ll only be loading the bodies of the documents that you need, instead of all of them.

—Jens

Ragu Vijaykumar

unread,
Jul 17, 2014, 12:32:01 AM7/17/14
to mobile-c...@googlegroups.com
Hmm, I just updated to this new code and all of a sudden, none of my CBLQuerys are returning any rows. I haven't changed anything else in my database. Is there something new that I need to do with these changes.

Jens Alfke

unread,
Jul 17, 2014, 3:00:28 AM7/17/14
to mobile-c...@googlegroups.com

On Jul 16, 2014, at 9:32 PM, Ragu Vijaykumar <ra...@scrxpt.com> wrote:

Hmm, I just updated to this new code and all of a sudden, none of my CBLQuerys are returning any rows. I haven't changed anything else in my database. Is there something new that I need to do with these changes.

No, everything should still work. What do your map functions and queries look like?

—Jens

Ragu Vijaykumar

unread,
Jul 17, 2014, 6:39:54 AM7/17/14
to mobile-c...@googlegroups.com
Here is an example of a query:

    static NSString* const kSCLabTagIdView = @"SCLabTagIdView";
    CBLDatabase* database = [[CBLManager sharedInstance] existingDatabaseNamed:kLabReferenceDatabase error:nil];
    CBLView* view = [database viewNamed:kSCLabTagIdView];
    if(!view.mapBlock) {
        [view setMapBlock:MAPBLOCK({
            if([doc[@"type"] isEqualToString:NSStringFromClass([LGTag class])] && doc[@"title"]) {
                emit(doc[@"tagId"], nil);
            }
        }) version:kApplicationVersionString];
    }
    
    return [view createQuery];


and I use it:

    self.labTagIdQuery = [[self.reference queryLabTagId] asLiveQuery];
    self.labTagIdQuery.limit = 30;
    [self.labTagIdQuery addObserver:self forKeyPath:@"rows" options:0 context:NULL];

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [self.labTagIdQuery.rows count];
}


With the new code, count is 0, whereas with the older codebase, it worked as expected. I've reverted back to the old code, and everything works as expected, so unclear what changed in this new code base to make my queries not perform as expected.

Jens Alfke

unread,
Jul 17, 2014, 1:02:45 PM7/17/14
to mobile-c...@googlegroups.com

On Jul 17, 2014, at 3:39 AM, Ragu Vijaykumar <ra...@scrxpt.com> wrote:

    self.labTagIdQuery = [[self.reference queryLabTagId] asLiveQuery];
    self.labTagIdQuery.limit = 30;
    [self.labTagIdQuery addObserver:self forKeyPath:@"rows" options:0 context:NULL];

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [self.labTagIdQuery.rows count];
}

There might be a race condition in your code that’s been triggered by some change in timing with the new code. CBLLiveQuery runs asynchronously, so its .rows property will initially be nil until the query has time to complete on the background thread. If your KV-observer method reloads the table view, you’re fine, but if not the view will get populated with whatever was in the query results at the time UIKit first loaded the table, which could be nil.

If that’s not the case — i.e. if labTagIdQuery either never notifies you or its rows remains empty after the notification — let me know. I just wrote a quick test case verifying that live queries still work correctly with views that emit nil values (as yours does.)

—Jens

PS: I did find a regression involving live queries on views that emit ‘doc’ as the value, and pushed a fix a few minutes ago.

Ragu Vijaykumar

unread,
Jul 17, 2014, 6:00:25 PM7/17/14
to mobile-c...@googlegroups.com
This is my observer code. It reloads the tableview, but still nothing comes through.

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
        [self.tableView reloadData];
}

I also have another tableview which I manually run a CBLQuery* to populate (not a CBLLiveQuery), and it too has issues where on the initial load, it doesn't think there are any rows. How do you think I should go about adjusting my code to work with your new changes?

Jens Alfke

unread,
Jul 18, 2014, 7:13:44 PM7/18/14
to mobile-c...@googlegroups.com

On Jul 17, 2014, at 3:00 PM, Ragu Vijaykumar <ra...@scrxpt.com> wrote:

I also have another tableview which I manually run a CBLQuery* to populate (not a CBLLiveQuery), and it too has issues where on the initial load, it doesn't think there are any rows. How do you think I should go about adjusting my code to work with your new changes?

You shouldn’t — I will need to fix my new changes to be compatible with your code.

Could you please turn on logging for “View” and “Query”, and post the logs that are output when you index and query the view?

—Jens
Message has been deleted
Message has been deleted

Ragu Vijaykumar

unread,
Jul 21, 2014, 11:37:18 AM7/21/14
to mobile-c...@googlegroups.com
I ran the view/query log functions for both the older version and newer updates in the CBL framework. I have changed nothing in the code other than dropping in the new compiled version of the framework.

New results: (returning 0 rows)

[                     AppDelegate (0x7d1196e0)]: application:didFinishLaunchingWithOptions:
[    SCTableViewController (0x79f3e820)]: viewDidLoad
08:28:51.953| View: Checking indexes of (SCPView)
08:28:51.954| View: Query SCPView  SELECT key, value, docid, revs.sequence FROM maps, revs, docs WHERE maps.view_id=? AND revs.sequence = maps.sequence AND docs.doc_id = revs.doc_id ORDER BY key DESC, docid DESC LIMIT ? OFFSET ?
Arguments: (
    "-1",
    "-1",
    0
)
08:28:51.955| View: Query SCPView: Returning 0 rows

Old Results: (returning 3 rows - CORRECT)

[                     AppDelegate (0x7b53b100)]: application:didFinishLaunchingWithOptions:
[    SCTableViewController (0x7b33cd20)]: viewDidLoad
08:23:54.768| View: Re-indexing view SCPView ...
08:23:54.770| View: Query SCPView  SELECT key, value, docid, revs.sequence FROM maps, revs, docs WHERE maps.view_id=? AND revs.sequence = maps.sequence AND docs.doc_id = revs.doc_id ORDER BY key DESC, docid DESC LIMIT ? OFFSET ?
Arguments: (
    1,
    "-1",
    0
)
08:23:54.771| View: Query SCPView: Returning 3 rows

Jens Alfke

unread,
Jul 21, 2014, 12:30:48 PM7/21/14
to mobile-c...@googlegroups.com

On Jul 21, 2014, at 8:37 AM, Ragu Vijaykumar <ra...@scrxpt.com> wrote:

Arguments: (
    "-1",

Thanks! That “-1” was a good clue. (It’s supposed to be the internal view ID, but the -1 is a placeholder. There was some code that was forgetting to look up the view ID the first time before putting it in the query.)

I just pushed a fix — can you update your sources and try again? Thanks a lot for your help so far.

If this still doesn’t fix it, can you change the “View” log keyword to “ViewVerbose” and send me the log output?

—Jens

Ragu Vijaykumar

unread,
Jul 21, 2014, 12:44:34 PM7/21/14
to mobile-c...@googlegroups.com
Everything seems to be working now. Thanks!

Jeremy Kelley

unread,
Aug 8, 2014, 3:19:43 PM8/8/14
to mobile-c...@googlegroups.com
Jens - Thanks for the heads up.  I hadn't actually realized that cb-lite didn't follow the same design doc mentality.  This will be a great performance boost!

Thanks!

-j
Reply all
Reply to author
Forward
0 new messages