Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions benchmark/performance.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,52 @@ async function benchmarkUserLogin() {
});
}

/**
* Benchmark: Query with Include (Parallel Include Pointers)
*/
async function benchmarkQueryWithInclude() {
// Setup: Create nested object hierarchy
const Level2Class = Parse.Object.extend('Level2');
const Level1Class = Parse.Object.extend('Level1');
const RootClass = Parse.Object.extend('Root');

// Create 10 Level2 objects
const level2Objects = [];
for (let i = 0; i < 10; i++) {
const obj = new Level2Class();
obj.set('name', `level2-${i}`);
obj.set('value', i);
level2Objects.push(obj);
}
await Parse.Object.saveAll(level2Objects);

// Create 10 Level1 objects, each pointing to a Level2 object
const level1Objects = [];
for (let i = 0; i < 10; i++) {
const obj = new Level1Class();
obj.set('name', `level1-${i}`);
obj.set('level2', level2Objects[i % level2Objects.length]);
level1Objects.push(obj);
}
await Parse.Object.saveAll(level1Objects);

// Create 10 Root objects, each pointing to a Level1 object
const rootObjects = [];
for (let i = 0; i < 10; i++) {
const obj = new RootClass();
obj.set('name', `root-${i}`);
obj.set('level1', level1Objects[i % level1Objects.length]);
rootObjects.push(obj);
}
await Parse.Object.saveAll(rootObjects);

return measureOperation('Query with Include (2 levels)', async () => {
const query = new Parse.Query('Root');
query.include('level1.level2');
await query.find();
}, Math.floor(ITERATIONS / 10)); // Fewer iterations for complex queries
}

/**
* Run all benchmarks
*/
Expand Down Expand Up @@ -341,6 +387,10 @@ async function runBenchmarks() {
await cleanupDatabase();
results.push(await benchmarkUserLogin());

console.log('Running Query with Include benchmark...');
await cleanupDatabase();
results.push(await benchmarkQueryWithInclude());

// Output results in github-action-benchmark format (stdout)
console.log(JSON.stringify(results, null, 2));

Expand Down
82 changes: 82 additions & 0 deletions spec/RestQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,88 @@ describe('rest query', () => {
}
);
});

it('battle test parallel include with 100 nested includes', async () => {
const RootObject = Parse.Object.extend('RootObject');
const Level1Object = Parse.Object.extend('Level1Object');
const Level2Object = Parse.Object.extend('Level2Object');

// Create 100 level2 objects (10 per level1 object)
const level2Objects = [];
for (let i = 0; i < 100; i++) {
const level2 = new Level2Object({
index: i,
value: `level2_${i}`,
});
level2Objects.push(level2);
}
await Parse.Object.saveAll(level2Objects);

// Create 10 level1 objects, each with 10 pointers to level2 objects
const level1Objects = [];
for (let i = 0; i < 10; i++) {
const level1 = new Level1Object({
index: i,
value: `level1_${i}`,
});
// Set 10 pointer fields (level2_0 through level2_9)
for (let j = 0; j < 10; j++) {
level1.set(`level2_${j}`, level2Objects[i * 10 + j]);
}
level1Objects.push(level1);
}
await Parse.Object.saveAll(level1Objects);

// Create 1 root object with 10 pointers to level1 objects
const rootObject = new RootObject({
value: 'root',
});
for (let i = 0; i < 10; i++) {
rootObject.set(`level1_${i}`, level1Objects[i]);
}
await rootObject.save();

// Build include paths: level1_0 through level1_9, and level1_0.level2_0 through level1_9.level2_9
const includePaths = [];
for (let i = 0; i < 10; i++) {
includePaths.push(`level1_${i}`);
for (let j = 0; j < 10; j++) {
includePaths.push(`level1_${i}.level2_${j}`);
}
}

// Query with all includes
const query = new Parse.Query(RootObject);
query.equalTo('objectId', rootObject.id);
for (const path of includePaths) {
query.include(path);
}
console.time('query.find');
const results = await query.find();
console.timeEnd('query.find');
expect(results.length).toBe(1);

const result = results[0];
expect(result.id).toBe(rootObject.id);

// Verify all 10 level1 objects are included
for (let i = 0; i < 10; i++) {
const level1Field = result.get(`level1_${i}`);
expect(level1Field).toBeDefined();
expect(level1Field instanceof Parse.Object).toBe(true);
expect(level1Field.get('index')).toBe(i);
expect(level1Field.get('value')).toBe(`level1_${i}`);

// Verify all 10 level2 objects are included for each level1 object
for (let j = 0; j < 10; j++) {
const level2Field = level1Field.get(`level2_${j}`);
expect(level2Field).toBeDefined();
expect(level2Field instanceof Parse.Object).toBe(true);
expect(level2Field.get('index')).toBe(i * 10 + j);
expect(level2Field.get('value')).toBe(`level2_${i * 10 + j}`);
}
}
});
});

describe('RestQuery.each', () => {
Expand Down
61 changes: 41 additions & 20 deletions src/RestQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -856,31 +856,54 @@ _UnsafeRestQuery.prototype.handleExcludeKeys = function () {
};

// Augments this.response with data at the paths provided in this.include.
_UnsafeRestQuery.prototype.handleInclude = function () {
_UnsafeRestQuery.prototype.handleInclude = async function () {
if (this.include.length == 0) {
return;
}

var pathResponse = includePath(
this.config,
this.auth,
this.response,
this.include[0],
this.context,
this.restOptions
);
if (pathResponse.then) {
return pathResponse.then(newResponse => {
this.response = newResponse;
this.include = this.include.slice(1);
return this.handleInclude();
const indexedResults = this.response.results.reduce((indexed, result, i) => {
indexed[result.objectId] = i;
return indexed;
}, {});

// Build the execution tree
const executionTree = {}
this.include.forEach(path => {
let current = executionTree;
path.forEach((node) => {
if (!current[node]) {
current[node] = {
path,
children: {}
};
}
current = current[node].children
});
} else if (this.include.length > 0) {
this.include = this.include.slice(1);
return this.handleInclude();
});

const recursiveExecutionTree = async (treeNode) => {
const { path, children } = treeNode;
const pathResponse = includePath(
this.config,
this.auth,
this.response,
path,
this.context,
this.restOptions,
this,
);
if (pathResponse.then) {
const newResponse = await pathResponse
newResponse.results.forEach(newObject => {
// We hydrate the root of each result with sub results
this.response.results[indexedResults[newObject.objectId]][path[0]] = newObject[path[0]];
})
}
return Promise.all(Object.values(children).map(recursiveExecutionTree));
}

return pathResponse;
await Promise.all(Object.values(executionTree).map(recursiveExecutionTree));
this.include = []
};

//Returns a promise of a processed set of results
Expand Down Expand Up @@ -1018,7 +1041,6 @@ function includePath(config, auth, response, path, context, restOptions = {}) {
} else if (restOptions.readPreference) {
includeRestOptions.readPreference = restOptions.readPreference;
}

const queryPromises = Object.keys(pointersHash).map(async className => {
const objectIds = Array.from(pointersHash[className]);
let where;
Expand Down Expand Up @@ -1057,7 +1079,6 @@ function includePath(config, auth, response, path, context, restOptions = {}) {
}
return replace;
}, {});

var resp = {
results: replacePointers(response.results, path, replace),
};
Expand Down
Loading