Skip to content

Commit dafea21

Browse files
authored
perf: Parse.Query.include now fetches pointers at same level in parallel (#9861)
1 parent 52f7c89 commit dafea21

File tree

4 files changed

+279
-81
lines changed

4 files changed

+279
-81
lines changed

.github/workflows/ci-performance.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
env:
7171
NODE_ENV: production
7272
run: |
73-
echo "Running baseline benchmarks with CPU affinity (using PR's benchmark script)..."
73+
echo "Running baseline benchmarks..."
7474
if [ ! -f "benchmark/performance.js" ]; then
7575
echo "⚠️ Benchmark script not found - this is expected for new features"
7676
echo "Skipping baseline benchmark"
@@ -135,7 +135,7 @@ jobs:
135135
env:
136136
NODE_ENV: production
137137
run: |
138-
echo "Running PR benchmarks with CPU affinity..."
138+
echo "Running PR benchmarks..."
139139
taskset -c 0 npm run benchmark > pr-output.txt 2>&1 || npm run benchmark > pr-output.txt 2>&1 || true
140140
echo "Benchmark command completed with exit code: $?"
141141
echo "Output file size: $(wc -c < pr-output.txt) bytes"

benchmark/performance.js

Lines changed: 154 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,11 @@ async function measureOperation({ name, operation, iterations, skipWarmup = fals
200200
/**
201201
* Benchmark: Object Create
202202
*/
203-
async function benchmarkObjectCreate() {
203+
async function benchmarkObjectCreate(name) {
204204
let counter = 0;
205205

206206
return measureOperation({
207-
name: 'Object Create',
207+
name,
208208
iterations: 1_000,
209209
operation: async () => {
210210
const TestObject = Parse.Object.extend('BenchmarkTest');
@@ -220,7 +220,7 @@ async function benchmarkObjectCreate() {
220220
/**
221221
* Benchmark: Object Read (by ID)
222222
*/
223-
async function benchmarkObjectRead() {
223+
async function benchmarkObjectRead(name) {
224224
// Setup: Create test objects
225225
const TestObject = Parse.Object.extend('BenchmarkTest');
226226
const objects = [];
@@ -236,7 +236,7 @@ async function benchmarkObjectRead() {
236236
let counter = 0;
237237

238238
return measureOperation({
239-
name: 'Object Read',
239+
name,
240240
iterations: 1_000,
241241
operation: async () => {
242242
const query = new Parse.Query('BenchmarkTest');
@@ -248,7 +248,7 @@ async function benchmarkObjectRead() {
248248
/**
249249
* Benchmark: Object Update
250250
*/
251-
async function benchmarkObjectUpdate() {
251+
async function benchmarkObjectUpdate(name) {
252252
// Setup: Create test objects
253253
const TestObject = Parse.Object.extend('BenchmarkTest');
254254
const objects = [];
@@ -265,7 +265,7 @@ async function benchmarkObjectUpdate() {
265265
let counter = 0;
266266

267267
return measureOperation({
268-
name: 'Object Update',
268+
name,
269269
iterations: 1_000,
270270
operation: async () => {
271271
const obj = objects[counter++ % objects.length];
@@ -279,7 +279,7 @@ async function benchmarkObjectUpdate() {
279279
/**
280280
* Benchmark: Simple Query
281281
*/
282-
async function benchmarkSimpleQuery() {
282+
async function benchmarkSimpleQuery(name) {
283283
// Setup: Create test data
284284
const TestObject = Parse.Object.extend('BenchmarkTest');
285285
const objects = [];
@@ -296,7 +296,7 @@ async function benchmarkSimpleQuery() {
296296
let counter = 0;
297297

298298
return measureOperation({
299-
name: 'Simple Query',
299+
name,
300300
iterations: 1_000,
301301
operation: async () => {
302302
const query = new Parse.Query('BenchmarkTest');
@@ -309,11 +309,11 @@ async function benchmarkSimpleQuery() {
309309
/**
310310
* Benchmark: Batch Save (saveAll)
311311
*/
312-
async function benchmarkBatchSave() {
312+
async function benchmarkBatchSave(name) {
313313
const BATCH_SIZE = 10;
314314

315315
return measureOperation({
316-
name: 'Batch Save (10 objects)',
316+
name,
317317
iterations: 1_000,
318318
operation: async () => {
319319
const TestObject = Parse.Object.extend('BenchmarkTest');
@@ -334,11 +334,11 @@ async function benchmarkBatchSave() {
334334
/**
335335
* Benchmark: User Signup
336336
*/
337-
async function benchmarkUserSignup() {
337+
async function benchmarkUserSignup(name) {
338338
let counter = 0;
339339

340340
return measureOperation({
341-
name: 'User Signup',
341+
name,
342342
iterations: 500,
343343
operation: async () => {
344344
counter++;
@@ -354,7 +354,7 @@ async function benchmarkUserSignup() {
354354
/**
355355
* Benchmark: User Login
356356
*/
357-
async function benchmarkUserLogin() {
357+
async function benchmarkUserLogin(name) {
358358
// Setup: Create test users
359359
const users = [];
360360

@@ -371,7 +371,7 @@ async function benchmarkUserLogin() {
371371
let counter = 0;
372372

373373
return measureOperation({
374-
name: 'User Login',
374+
name,
375375
iterations: 500,
376376
operation: async () => {
377377
const userCreds = users[counter++ % users.length];
@@ -382,52 +382,146 @@ async function benchmarkUserLogin() {
382382
}
383383

384384
/**
385-
* Benchmark: Query with Include (Parallel Include Pointers)
385+
* Benchmark: Query with Include (Parallel Pointers)
386+
* Tests the performance improvement when fetching multiple pointers at the same level.
386387
*/
387-
async function benchmarkQueryWithInclude() {
388-
// Setup: Create nested object hierarchy
388+
async function benchmarkQueryWithIncludeParallel(name) {
389+
const PointerAClass = Parse.Object.extend('PointerA');
390+
const PointerBClass = Parse.Object.extend('PointerB');
391+
const PointerCClass = Parse.Object.extend('PointerC');
392+
const RootClass = Parse.Object.extend('Root');
393+
394+
// Create pointer objects
395+
const pointerAObjects = [];
396+
for (let i = 0; i < 10; i++) {
397+
const obj = new PointerAClass();
398+
obj.set('name', `pointerA-${i}`);
399+
pointerAObjects.push(obj);
400+
}
401+
await Parse.Object.saveAll(pointerAObjects);
402+
403+
const pointerBObjects = [];
404+
for (let i = 0; i < 10; i++) {
405+
const obj = new PointerBClass();
406+
obj.set('name', `pointerB-${i}`);
407+
pointerBObjects.push(obj);
408+
}
409+
await Parse.Object.saveAll(pointerBObjects);
410+
411+
const pointerCObjects = [];
412+
for (let i = 0; i < 10; i++) {
413+
const obj = new PointerCClass();
414+
obj.set('name', `pointerC-${i}`);
415+
pointerCObjects.push(obj);
416+
}
417+
await Parse.Object.saveAll(pointerCObjects);
418+
419+
// Create Root objects with multiple pointers at the same level
420+
const rootObjects = [];
421+
for (let i = 0; i < 10; i++) {
422+
const obj = new RootClass();
423+
obj.set('name', `root-${i}`);
424+
obj.set('pointerA', pointerAObjects[i % pointerAObjects.length]);
425+
obj.set('pointerB', pointerBObjects[i % pointerBObjects.length]);
426+
obj.set('pointerC', pointerCObjects[i % pointerCObjects.length]);
427+
rootObjects.push(obj);
428+
}
429+
await Parse.Object.saveAll(rootObjects);
430+
431+
return measureOperation({
432+
name,
433+
skipWarmup: true,
434+
dbLatency: 100,
435+
iterations: 100,
436+
operation: async () => {
437+
const query = new Parse.Query('Root');
438+
// Include multiple pointers at the same level - should fetch in parallel
439+
query.include(['pointerA', 'pointerB', 'pointerC']);
440+
await query.find();
441+
},
442+
});
443+
}
444+
445+
/**
446+
* Benchmark: Query with Include (Nested Pointers with Parallel Leaf Nodes)
447+
* Tests the PR's optimization for parallel fetching at each nested level.
448+
* Pattern: p1.p2.p3, p1.p2.p4, p1.p2.p5
449+
* After fetching p2, we know the objectIds and can fetch p3, p4, p5 in parallel.
450+
*/
451+
async function benchmarkQueryWithIncludeNested(name) {
452+
const Level3AClass = Parse.Object.extend('Level3A');
453+
const Level3BClass = Parse.Object.extend('Level3B');
454+
const Level3CClass = Parse.Object.extend('Level3C');
389455
const Level2Class = Parse.Object.extend('Level2');
390456
const Level1Class = Parse.Object.extend('Level1');
391457
const RootClass = Parse.Object.extend('Root');
392458

459+
// Create Level3 objects (leaf nodes)
460+
const level3AObjects = [];
461+
for (let i = 0; i < 10; i++) {
462+
const obj = new Level3AClass();
463+
obj.set('name', `level3A-${i}`);
464+
level3AObjects.push(obj);
465+
}
466+
await Parse.Object.saveAll(level3AObjects);
467+
468+
const level3BObjects = [];
469+
for (let i = 0; i < 10; i++) {
470+
const obj = new Level3BClass();
471+
obj.set('name', `level3B-${i}`);
472+
level3BObjects.push(obj);
473+
}
474+
await Parse.Object.saveAll(level3BObjects);
475+
476+
const level3CObjects = [];
477+
for (let i = 0; i < 10; i++) {
478+
const obj = new Level3CClass();
479+
obj.set('name', `level3C-${i}`);
480+
level3CObjects.push(obj);
481+
}
482+
await Parse.Object.saveAll(level3CObjects);
483+
484+
// Create Level2 objects pointing to multiple Level3 objects
485+
const level2Objects = [];
486+
for (let i = 0; i < 10; i++) {
487+
const obj = new Level2Class();
488+
obj.set('name', `level2-${i}`);
489+
obj.set('level3A', level3AObjects[i % level3AObjects.length]);
490+
obj.set('level3B', level3BObjects[i % level3BObjects.length]);
491+
obj.set('level3C', level3CObjects[i % level3CObjects.length]);
492+
level2Objects.push(obj);
493+
}
494+
await Parse.Object.saveAll(level2Objects);
495+
496+
// Create Level1 objects pointing to Level2
497+
const level1Objects = [];
498+
for (let i = 0; i < 10; i++) {
499+
const obj = new Level1Class();
500+
obj.set('name', `level1-${i}`);
501+
obj.set('level2', level2Objects[i % level2Objects.length]);
502+
level1Objects.push(obj);
503+
}
504+
await Parse.Object.saveAll(level1Objects);
505+
506+
// Create Root objects pointing to Level1
507+
const rootObjects = [];
508+
for (let i = 0; i < 10; i++) {
509+
const obj = new RootClass();
510+
obj.set('name', `root-${i}`);
511+
obj.set('level1', level1Objects[i % level1Objects.length]);
512+
rootObjects.push(obj);
513+
}
514+
await Parse.Object.saveAll(rootObjects);
515+
393516
return measureOperation({
394-
name: 'Query with Include (2 levels)',
517+
name,
395518
skipWarmup: true,
396-
dbLatency: 50,
519+
dbLatency: 100,
397520
iterations: 100,
398521
operation: async () => {
399-
// Create 10 Level2 objects
400-
const level2Objects = [];
401-
for (let i = 0; i < 10; i++) {
402-
const obj = new Level2Class();
403-
obj.set('name', `level2-${i}`);
404-
obj.set('value', i);
405-
level2Objects.push(obj);
406-
}
407-
await Parse.Object.saveAll(level2Objects);
408-
409-
// Create 10 Level1 objects, each pointing to a Level2 object
410-
const level1Objects = [];
411-
for (let i = 0; i < 10; i++) {
412-
const obj = new Level1Class();
413-
obj.set('name', `level1-${i}`);
414-
obj.set('level2', level2Objects[i % level2Objects.length]);
415-
level1Objects.push(obj);
416-
}
417-
await Parse.Object.saveAll(level1Objects);
418-
419-
// Create 10 Root objects, each pointing to a Level1 object
420-
const rootObjects = [];
421-
for (let i = 0; i < 10; i++) {
422-
const obj = new RootClass();
423-
obj.set('name', `root-${i}`);
424-
obj.set('level1', level1Objects[i % level1Objects.length]);
425-
rootObjects.push(obj);
426-
}
427-
await Parse.Object.saveAll(rootObjects);
428-
429522
const query = new Parse.Query('Root');
430-
query.include('level1.level2');
523+
// After fetching level1.level2, the PR should fetch level3A, level3B, level3C in parallel
524+
query.include(['level1.level2.level3A', 'level1.level2.level3B', 'level1.level2.level3C']);
431525
await query.find();
432526
},
433527
});
@@ -453,22 +547,23 @@ async function runBenchmarks() {
453547

454548
// Define all benchmarks to run
455549
const benchmarks = [
456-
{ name: 'Object Create', fn: benchmarkObjectCreate },
457-
{ name: 'Object Read', fn: benchmarkObjectRead },
458-
{ name: 'Object Update', fn: benchmarkObjectUpdate },
459-
{ name: 'Simple Query', fn: benchmarkSimpleQuery },
460-
{ name: 'Batch Save', fn: benchmarkBatchSave },
461-
{ name: 'User Signup', fn: benchmarkUserSignup },
462-
{ name: 'User Login', fn: benchmarkUserLogin },
463-
{ name: 'Query with Include', fn: benchmarkQueryWithInclude },
550+
{ name: 'Object.save (create)', fn: benchmarkObjectCreate },
551+
{ name: 'Object.save (update)', fn: benchmarkObjectUpdate },
552+
{ name: 'Object.saveAll (batch save)', fn: benchmarkBatchSave },
553+
{ name: 'Query.get (by objectId)', fn: benchmarkObjectRead },
554+
{ name: 'Query.find (simple query)', fn: benchmarkSimpleQuery },
555+
{ name: 'User.signUp', fn: benchmarkUserSignup },
556+
{ name: 'User.login', fn: benchmarkUserLogin },
557+
{ name: 'Query.include (parallel pointers)', fn: benchmarkQueryWithIncludeParallel },
558+
{ name: 'Query.include (nested pointers)', fn: benchmarkQueryWithIncludeNested },
464559
];
465560

466561
// Run each benchmark with database cleanup
467562
for (const benchmark of benchmarks) {
468563
logInfo(`\nRunning benchmark '${benchmark.name}'...`);
469564
resetParseServer();
470565
await cleanupDatabase();
471-
results.push(await benchmark.fn());
566+
results.push(await benchmark.fn(benchmark.name));
472567
}
473568

474569
// Output results in github-action-benchmark format (stdout)

0 commit comments

Comments
 (0)