Skip to content

Commit e1c1345

Browse files
committed
Fetch multiple includes concurrently
When preloading multiple fields, we can get some extra performance by doing multiple fetches simultaneously. Consider a query fetching comments, and requesting that ParseServer preload "post" and "author" pointers: > GET /classes/Comment?include=post,author In this case, we first need to fetch the resulting "Comment" documents, but after that we should be able to fetch the documents referenced by the `post` and `author` fields of the results simultaneously, as they don't depend on each other at all. Things get a little trickier when we have nested fields: > GET /classes/Comment?include=post,post.author,post.category To resolve this query, we first need to fetch the related posts, and only once we've added the data about those posts into the results tree can we scan it for the `post.author` and `post.category` pointers. But, once that first fetch is completed, we can unblock both of those nested queries! The solution here is to build what is basically a dependency graph out of promises; each include path blocks while it is waiting for whatever path it depends on to finish loading. Finally, once we have that graph, we return a promise that depends on every node in it. Aside: Technically we only need to depend on leaf nodes, but there shouldn't be any meaningful difference between waiting on the leafs and waiting on the entire graph, and this way we don't have to do any analysis to find the leafs) It's possible that for degenerate cases (hundreds of includes in a single query), this could result in performance degradation as many queries are kicked off in parallel. For the more common case of just a few top level includes, this should be a noticeable speed up as we remove a "waterfall" style dependency graph. Improve readability of `RestQuery.handleInclude` There's quite a bit of trickiness going on here, so for the benefit of future maintainers, lets add some documentation and clear up some variable names. - Documented the preconditions that we expect when we're entering this function. These are all met by the current implementation of the caller, but it's helpful to someone trying to verify the correctness of this function. - Added a lengthier description of what is going on with the map of `promises` - Renamed some variables to help make their purpose clearer
1 parent e43a843 commit e1c1345

File tree

1 file changed

+50
-16
lines changed

1 file changed

+50
-16
lines changed

src/RestQuery.js

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -747,27 +747,61 @@ RestQuery.prototype.handleExcludeKeys = function() {
747747
};
748748

749749
// Augments this.response with data at the paths provided in this.include.
750+
//
751+
// Preconditions:
752+
// - `this.include` is an array of arrays of strings; (in flow parlance, Array<Array<string>>)
753+
//
754+
// - `this.include` is de-duplicated. This ensures that we don't try to fetch
755+
// the same objects twice.
756+
//
757+
// - For each value in `this.include` with length > 1, there is also
758+
// an earlier value for the prefix of that value.
759+
//
760+
// Example: ['a', 'b', 'c'] in the array implies that ['a', 'b'] is also in
761+
// the array, at an earlier position).
762+
//
763+
// This prevents trying to follow pointers on unfetched objects.
750764
RestQuery.prototype.handleInclude = function() {
751765
if (this.include.length == 0) {
752766
return;
753767
}
754768

755-
var pathResponse = includePath(
756-
this.config,
757-
this.auth,
758-
this.response,
759-
this.include[0],
760-
this.restOptions
761-
);
762-
if (pathResponse.then) {
763-
return pathResponse.then(() => {
764-
this.include = this.include.slice(1);
765-
return this.handleInclude();
766-
});
767-
} else if (this.include.length > 0) {
768-
this.include = this.include.slice(1);
769-
return this.handleInclude();
770-
}
769+
// The list of includes form a sort of a tree - Each path should wait to
770+
// start trying to load until its parent path has finished loading (so that
771+
// the pointers it is trying to read and fetch are in the object tree).
772+
//
773+
// So, for instance, if we have an include of ['a', 'b', 'c'], that must
774+
// wait on the include of ['a', 'b'] to finish, which must wait on the include
775+
// of ['a'] to finish.
776+
//
777+
// This `promises` object is a map of dotted paths to promises that resolve
778+
// when that path has finished loading into the tree. One special case is the
779+
// empty path (represented by the empty string). This represents the root of
780+
// the tree, which is `this.response` and is already fetched. We set a
781+
// pre-resolved promise at that level, meaning that include paths with only
782+
// one component (like `['a']`) will chain onto that resolved promise and
783+
// are immediately unblocked.
784+
const promises = { '': Promise.resolve() };
785+
786+
this.include.forEach(path => {
787+
const dottedPath = path.join('.');
788+
789+
// Get the promise for the parent path
790+
const parentDottedPath = path.slice(0, -1).join('.');
791+
const parentPromise = promises[parentDottedPath];
792+
793+
// Once the parent promise has resolved, do this path's load step
794+
const loadPromise = parentPromise.then(() =>
795+
includePath(this.config, this.auth, this.response, path, this.restOptions)
796+
);
797+
798+
// Put our promise into the promises map, so child paths can find and chain
799+
// off of it
800+
promises[dottedPath] = loadPromise;
801+
});
802+
803+
// Wait for all includes to be fetched and merged in to the response tree
804+
return Promise.all(Object.values(promises));
771805
};
772806

773807
//Returns a promise of a processed set of results

0 commit comments

Comments
 (0)