Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
daaeed8
fix: mongodb-runner usage and default version to 6.0.2
Moumouls Oct 26, 2024
5811465
fix: json
Moumouls Oct 26, 2024
6e89fd1
feat: parallel include
Moumouls Oct 28, 2024
28ffcd7
Merge branch 'alpha' of github.com:parse-community/parse-server into …
Moumouls Sep 13, 2025
328a4be
Merge branch 'alpha' of github.com:parse-community/parse-server into …
Moumouls Nov 8, 2025
8b99dc5
feat: add test to battle test include
Moumouls Nov 8, 2025
d854752
Merge branch 'alpha' into moumouls/concurrent-include
Moumouls Nov 8, 2025
c613e3f
Merge branch 'alpha' into moumouls/concurrent-include
mtrezza Nov 8, 2025
af50fd3
Merge branch 'alpha' into moumouls/concurrent-include
mtrezza Nov 9, 2025
87339eb
add perf test
mtrezza Nov 9, 2025
2618e96
Merge branch 'alpha' into moumouls/concurrent-include
mtrezza Nov 9, 2025
e46dba6
Merge branch 'alpha' into moumouls/concurrent-include
mtrezza Nov 9, 2025
b7c919d
Merge branch 'alpha' into moumouls/concurrent-include
mtrezza Nov 9, 2025
d432e4c
less verbose
mtrezza Nov 9, 2025
bb74681
db proxy
mtrezza Nov 9, 2025
0bef43f
refactor
mtrezza Nov 9, 2025
97bb64b
Merge branch 'alpha' into moumouls/concurrent-include
mtrezza Nov 9, 2025
713a4d0
remove proxy
mtrezza Nov 9, 2025
0e17c8f
fix
mtrezza Nov 9, 2025
35f1809
Merge branch 'moumouls/concurrent-include' of https://github.com/Moum…
mtrezza Nov 9, 2025
96e8f1f
logging
mtrezza Nov 9, 2025
375c807
db latency
mtrezza Nov 9, 2025
b8ccc4a
Update MongoLatencyWrapper.js
mtrezza Nov 16, 2025
6840b60
schema concurrency fix
mtrezza Nov 16, 2025
523b886
Revert "schema concurrency fix"
mtrezza Nov 16, 2025
e4dbfdf
all benchmarks
mtrezza Nov 16, 2025
735d506
faster
mtrezza Nov 16, 2025
199b726
Merge branch 'alpha' into moumouls/concurrent-include
mtrezza Nov 17, 2025
170ce00
fix
mtrezza Nov 17, 2025
24312ec
fix benchmark
mtrezza Nov 17, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/ci-performance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
env:
NODE_ENV: production
run: |
echo "Running baseline benchmarks with CPU affinity (using PR's benchmark script)..."
echo "Running baseline benchmarks..."
if [ ! -f "benchmark/performance.js" ]; then
echo "⚠️ Benchmark script not found - this is expected for new features"
echo "Skipping baseline benchmark"
Expand Down Expand Up @@ -135,7 +135,7 @@ jobs:
env:
NODE_ENV: production
run: |
echo "Running PR benchmarks with CPU affinity..."
echo "Running PR benchmarks..."
taskset -c 0 npm run benchmark > pr-output.txt 2>&1 || npm run benchmark > pr-output.txt 2>&1 || true
echo "Benchmark command completed with exit code: $?"
echo "Output file size: $(wc -c < pr-output.txt) bytes"
Expand Down
213 changes: 154 additions & 59 deletions benchmark/performance.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,11 @@ async function measureOperation({ name, operation, iterations, skipWarmup = fals
/**
* Benchmark: Object Create
*/
async function benchmarkObjectCreate() {
async function benchmarkObjectCreate(name) {
let counter = 0;

return measureOperation({
name: 'Object Create',
name,
iterations: 1_000,
operation: async () => {
const TestObject = Parse.Object.extend('BenchmarkTest');
Expand All @@ -220,7 +220,7 @@ async function benchmarkObjectCreate() {
/**
* Benchmark: Object Read (by ID)
*/
async function benchmarkObjectRead() {
async function benchmarkObjectRead(name) {
// Setup: Create test objects
const TestObject = Parse.Object.extend('BenchmarkTest');
const objects = [];
Expand All @@ -236,7 +236,7 @@ async function benchmarkObjectRead() {
let counter = 0;

return measureOperation({
name: 'Object Read',
name,
iterations: 1_000,
operation: async () => {
const query = new Parse.Query('BenchmarkTest');
Expand All @@ -248,7 +248,7 @@ async function benchmarkObjectRead() {
/**
* Benchmark: Object Update
*/
async function benchmarkObjectUpdate() {
async function benchmarkObjectUpdate(name) {
// Setup: Create test objects
const TestObject = Parse.Object.extend('BenchmarkTest');
const objects = [];
Expand All @@ -265,7 +265,7 @@ async function benchmarkObjectUpdate() {
let counter = 0;

return measureOperation({
name: 'Object Update',
name,
iterations: 1_000,
operation: async () => {
const obj = objects[counter++ % objects.length];
Expand All @@ -279,7 +279,7 @@ async function benchmarkObjectUpdate() {
/**
* Benchmark: Simple Query
*/
async function benchmarkSimpleQuery() {
async function benchmarkSimpleQuery(name) {
// Setup: Create test data
const TestObject = Parse.Object.extend('BenchmarkTest');
const objects = [];
Expand All @@ -296,7 +296,7 @@ async function benchmarkSimpleQuery() {
let counter = 0;

return measureOperation({
name: 'Simple Query',
name,
iterations: 1_000,
operation: async () => {
const query = new Parse.Query('BenchmarkTest');
Expand All @@ -309,11 +309,11 @@ async function benchmarkSimpleQuery() {
/**
* Benchmark: Batch Save (saveAll)
*/
async function benchmarkBatchSave() {
async function benchmarkBatchSave(name) {
const BATCH_SIZE = 10;

return measureOperation({
name: 'Batch Save (10 objects)',
name,
iterations: 1_000,
operation: async () => {
const TestObject = Parse.Object.extend('BenchmarkTest');
Expand All @@ -334,11 +334,11 @@ async function benchmarkBatchSave() {
/**
* Benchmark: User Signup
*/
async function benchmarkUserSignup() {
async function benchmarkUserSignup(name) {
let counter = 0;

return measureOperation({
name: 'User Signup',
name,
iterations: 500,
operation: async () => {
counter++;
Expand All @@ -354,7 +354,7 @@ async function benchmarkUserSignup() {
/**
* Benchmark: User Login
*/
async function benchmarkUserLogin() {
async function benchmarkUserLogin(name) {
// Setup: Create test users
const users = [];

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

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

/**
* Benchmark: Query with Include (Parallel Include Pointers)
* Benchmark: Query with Include (Parallel Pointers)
* Tests the performance improvement when fetching multiple pointers at the same level.
*/
async function benchmarkQueryWithInclude() {
// Setup: Create nested object hierarchy
async function benchmarkQueryWithIncludeParallel(name) {
const PointerAClass = Parse.Object.extend('PointerA');
const PointerBClass = Parse.Object.extend('PointerB');
const PointerCClass = Parse.Object.extend('PointerC');
const RootClass = Parse.Object.extend('Root');

// Create pointer objects
const pointerAObjects = [];
for (let i = 0; i < 10; i++) {
const obj = new PointerAClass();
obj.set('name', `pointerA-${i}`);
pointerAObjects.push(obj);
}
await Parse.Object.saveAll(pointerAObjects);

const pointerBObjects = [];
for (let i = 0; i < 10; i++) {
const obj = new PointerBClass();
obj.set('name', `pointerB-${i}`);
pointerBObjects.push(obj);
}
await Parse.Object.saveAll(pointerBObjects);

const pointerCObjects = [];
for (let i = 0; i < 10; i++) {
const obj = new PointerCClass();
obj.set('name', `pointerC-${i}`);
pointerCObjects.push(obj);
}
await Parse.Object.saveAll(pointerCObjects);

// Create Root objects with multiple pointers at the same level
const rootObjects = [];
for (let i = 0; i < 10; i++) {
const obj = new RootClass();
obj.set('name', `root-${i}`);
obj.set('pointerA', pointerAObjects[i % pointerAObjects.length]);
obj.set('pointerB', pointerBObjects[i % pointerBObjects.length]);
obj.set('pointerC', pointerCObjects[i % pointerCObjects.length]);
rootObjects.push(obj);
}
await Parse.Object.saveAll(rootObjects);

return measureOperation({
name,
skipWarmup: true,
dbLatency: 100,
iterations: 100,
operation: async () => {
const query = new Parse.Query('Root');
// Include multiple pointers at the same level - should fetch in parallel
query.include(['pointerA', 'pointerB', 'pointerC']);
await query.find();
},
});
}

/**
* Benchmark: Query with Include (Nested Pointers with Parallel Leaf Nodes)
* Tests the PR's optimization for parallel fetching at each nested level.
* Pattern: p1.p2.p3, p1.p2.p4, p1.p2.p5
* After fetching p2, we know the objectIds and can fetch p3, p4, p5 in parallel.
*/
async function benchmarkQueryWithIncludeNested(name) {
const Level3AClass = Parse.Object.extend('Level3A');
const Level3BClass = Parse.Object.extend('Level3B');
const Level3CClass = Parse.Object.extend('Level3C');
const Level2Class = Parse.Object.extend('Level2');
const Level1Class = Parse.Object.extend('Level1');
const RootClass = Parse.Object.extend('Root');

// Create Level3 objects (leaf nodes)
const level3AObjects = [];
for (let i = 0; i < 10; i++) {
const obj = new Level3AClass();
obj.set('name', `level3A-${i}`);
level3AObjects.push(obj);
}
await Parse.Object.saveAll(level3AObjects);

const level3BObjects = [];
for (let i = 0; i < 10; i++) {
const obj = new Level3BClass();
obj.set('name', `level3B-${i}`);
level3BObjects.push(obj);
}
await Parse.Object.saveAll(level3BObjects);

const level3CObjects = [];
for (let i = 0; i < 10; i++) {
const obj = new Level3CClass();
obj.set('name', `level3C-${i}`);
level3CObjects.push(obj);
}
await Parse.Object.saveAll(level3CObjects);

// Create Level2 objects pointing to multiple Level3 objects
const level2Objects = [];
for (let i = 0; i < 10; i++) {
const obj = new Level2Class();
obj.set('name', `level2-${i}`);
obj.set('level3A', level3AObjects[i % level3AObjects.length]);
obj.set('level3B', level3BObjects[i % level3BObjects.length]);
obj.set('level3C', level3CObjects[i % level3CObjects.length]);
level2Objects.push(obj);
}
await Parse.Object.saveAll(level2Objects);

// Create Level1 objects pointing to Level2
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 Root objects pointing to Level1
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({
name: 'Query with Include (2 levels)',
name,
skipWarmup: true,
dbLatency: 50,
dbLatency: 100,
iterations: 100,
operation: async () => {
// 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);

const query = new Parse.Query('Root');
query.include('level1.level2');
// After fetching level1.level2, the PR should fetch level3A, level3B, level3C in parallel
query.include(['level1.level2.level3A', 'level1.level2.level3B', 'level1.level2.level3C']);
await query.find();
},
});
Expand All @@ -453,22 +547,23 @@ async function runBenchmarks() {

// Define all benchmarks to run
const benchmarks = [
{ name: 'Object Create', fn: benchmarkObjectCreate },
{ name: 'Object Read', fn: benchmarkObjectRead },
{ name: 'Object Update', fn: benchmarkObjectUpdate },
{ name: 'Simple Query', fn: benchmarkSimpleQuery },
{ name: 'Batch Save', fn: benchmarkBatchSave },
{ name: 'User Signup', fn: benchmarkUserSignup },
{ name: 'User Login', fn: benchmarkUserLogin },
{ name: 'Query with Include', fn: benchmarkQueryWithInclude },
{ name: 'Object.save (create)', fn: benchmarkObjectCreate },
{ name: 'Object.save (update)', fn: benchmarkObjectUpdate },
{ name: 'Object.saveAll (batch save)', fn: benchmarkBatchSave },
{ name: 'Query.get (by objectId)', fn: benchmarkObjectRead },
{ name: 'Query.find (simple query)', fn: benchmarkSimpleQuery },
{ name: 'User.signUp', fn: benchmarkUserSignup },
{ name: 'User.login', fn: benchmarkUserLogin },
{ name: 'Query.include (parallel pointers)', fn: benchmarkQueryWithIncludeParallel },
{ name: 'Query.include (nested pointers)', fn: benchmarkQueryWithIncludeNested },
];

// Run each benchmark with database cleanup
for (const benchmark of benchmarks) {
logInfo(`\nRunning benchmark '${benchmark.name}'...`);
resetParseServer();
await cleanupDatabase();
results.push(await benchmark.fn());
results.push(await benchmark.fn(benchmark.name));
}

// Output results in github-action-benchmark format (stdout)
Expand Down
Loading
Loading