From daaeed8df396b9fa9611cf41dbea3814668a603f Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sat, 26 Oct 2024 09:34:37 +0200 Subject: [PATCH 01/11] fix: mongodb-runner usage and default version to 6.0.2 --- .gitignore | 2 ++ package.json | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index e4e19156c2..7c65ffb638 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ lib/ # Redis Dump dump.rdb + +_mongodb_runner \ No newline at end of file diff --git a/package.json b/package.json index 26d9d591d2..1f0aab11a2 100644 --- a/package.json +++ b/package.json @@ -127,16 +127,16 @@ "test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2", "test:mongodb:7.0.1": "npm run test:mongodb --dbversion=7.0.1", "test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly", - "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017", - "testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine", + "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} --runnerDir ./_mongodb_runner -- --port 27017", + "testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine", "test": "npm run testonly", - "posttest": "cross-env mongodb-runner stop --all", - "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine", + "posttest": "cross-env mongodb-runner stop --all --runnerDir ./_mongodb_runner", + "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine", "start": "node ./bin/parse-server", "prettier": "prettier --write {src,spec}/{**/*,*}.js", "prepare": "npm run build", "postinstall": "node -p 'require(\"./postinstall.js\")()'", - "madge:circular": "node_modules/.bin/madge ./src --circular" + "madge:circular": "node_modules/.bin/madge ./src --circular", }, "engines": { "node": ">=18.0.0 <19.0.0 || >=19.0.0 <20.0.0 || >=20.0.0 <21.0.0 || >=22.0.0 <23.0.0" From 5811465aaa3189a64bb548b99e1e49fb39fc8481 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sat, 26 Oct 2024 09:42:14 +0200 Subject: [PATCH 02/11] fix: json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f0aab11a2..77fa2ff18e 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "prettier": "prettier --write {src,spec}/{**/*,*}.js", "prepare": "npm run build", "postinstall": "node -p 'require(\"./postinstall.js\")()'", - "madge:circular": "node_modules/.bin/madge ./src --circular", + "madge:circular": "node_modules/.bin/madge ./src --circular" }, "engines": { "node": ">=18.0.0 <19.0.0 || >=19.0.0 <20.0.0 || >=20.0.0 <21.0.0 || >=22.0.0 <23.0.0" From 6e89fd16769bfa9efb3869aae86c1e034281a88b Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Mon, 28 Oct 2024 19:28:02 +0100 Subject: [PATCH 03/11] feat: parallel include --- src/RestQuery.js | 61 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/RestQuery.js b/src/RestQuery.js index 621700984b..dd1992e56b 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -852,31 +852,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 @@ -1013,7 +1036,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; @@ -1052,7 +1074,6 @@ function includePath(config, auth, response, path, context, restOptions = {}) { } return replace; }, {}); - var resp = { results: replacePointers(response.results, path, replace), }; From 8b99dc5ed70c41bc1a3f63f8d1f8de21162d3870 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sat, 8 Nov 2025 20:01:03 +0100 Subject: [PATCH 04/11] feat: add test to battle test include --- spec/RestQuery.spec.js | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 6fe3c0fa18..7b676da1ea 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -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', () => { From 87339eb30a5039751cb8461f634f1ef5740378e7 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:17:58 +0100 Subject: [PATCH 05/11] add perf test --- benchmark/performance.js | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/benchmark/performance.js b/benchmark/performance.js index 7021ed35b3..55033916ab 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -293,6 +293,52 @@ async function benchmarkUserLogin() { }, Math.floor(ITERATIONS / 10)); // Fewer iterations for user operations } +/** + * 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 */ @@ -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)); From d432e4c077cb381ca51e86b4845387cd2c662058 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 13:52:07 +0100 Subject: [PATCH 06/11] less verbose --- .github/workflows/ci-performance.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-performance.yml b/.github/workflows/ci-performance.yml index e3afe3626f..b44e4c2383 100644 --- a/.github/workflows/ci-performance.yml +++ b/.github/workflows/ci-performance.yml @@ -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" @@ -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" From bb746818afa657c5e7dd62c07ec462adb5f02e61 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 13:52:11 +0100 Subject: [PATCH 07/11] db proxy --- benchmark/db-proxy.js | 69 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 benchmark/db-proxy.js diff --git a/benchmark/db-proxy.js b/benchmark/db-proxy.js new file mode 100644 index 0000000000..33079257eb --- /dev/null +++ b/benchmark/db-proxy.js @@ -0,0 +1,69 @@ +/** + * Simple TCP proxy to add artificial latency to MongoDB connections + * This helps make benchmark measurements more stable by simulating network conditions + */ + +const net = require('net'); + +const PROXY_PORT = parseInt(process.env.PROXY_PORT || '27018', 10); +const TARGET_HOST = process.env.TARGET_HOST || 'localhost'; +const TARGET_PORT = parseInt(process.env.TARGET_PORT || '27017', 10); +const LATENCY_MS = parseInt(process.env.LATENCY_MS || '10', 10); + +const server = net.createServer((clientSocket) => { + const serverSocket = net.createConnection({ + host: TARGET_HOST, + port: TARGET_PORT, + }); + + // Add latency to data flowing from client to MongoDB + clientSocket.on('data', (data) => { + setTimeout(() => { + if (!serverSocket.destroyed) { + serverSocket.write(data); + } + }, LATENCY_MS); + }); + + // Add latency to data flowing from MongoDB to client + serverSocket.on('data', (data) => { + setTimeout(() => { + if (!clientSocket.destroyed) { + clientSocket.write(data); + } + }, LATENCY_MS); + }); + + clientSocket.on('error', () => { + serverSocket.destroy(); + }); + + serverSocket.on('error', () => { + clientSocket.destroy(); + }); + + clientSocket.on('close', () => { + serverSocket.destroy(); + }); + + serverSocket.on('close', () => { + clientSocket.destroy(); + }); +}); + +server.listen(PROXY_PORT, () => { + console.log(`MongoDB proxy listening on port ${PROXY_PORT}`); + console.log(`Forwarding to ${TARGET_HOST}:${TARGET_PORT} with ${LATENCY_MS}ms latency`); +}); + +process.on('SIGTERM', () => { + server.close(() => { + process.exit(0); + }); +}); + +process.on('SIGINT', () => { + server.close(() => { + process.exit(0); + }); +}); From 0bef43fa83d28cf6717655728e51366dda4c3324 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:02:46 +0100 Subject: [PATCH 08/11] refactor --- benchmark/db-proxy.js | 5 +- benchmark/performance.js | 155 ++++++++++++++++++++++++++++++--------- 2 files changed, 124 insertions(+), 36 deletions(-) diff --git a/benchmark/db-proxy.js b/benchmark/db-proxy.js index 33079257eb..bdae86bc69 100644 --- a/benchmark/db-proxy.js +++ b/benchmark/db-proxy.js @@ -3,6 +3,8 @@ * This helps make benchmark measurements more stable by simulating network conditions */ +/* eslint-disable no-console */ + const net = require('net'); const PROXY_PORT = parseInt(process.env.PROXY_PORT || '27018', 10); @@ -52,8 +54,7 @@ const server = net.createServer((clientSocket) => { }); server.listen(PROXY_PORT, () => { - console.log(`MongoDB proxy listening on port ${PROXY_PORT}`); - console.log(`Forwarding to ${TARGET_HOST}:${TARGET_PORT} with ${LATENCY_MS}ms latency`); + console.log(`MongoDB proxy listening on port ${PROXY_PORT} forwarding to ${TARGET_PORT} with ${LATENCY_MS}ms latency`); }); process.on('SIGTERM', () => { diff --git a/benchmark/performance.js b/benchmark/performance.js index 31a2f276d6..dd2305f676 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -24,6 +24,35 @@ const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '10000', 10); // Parse Server instance let parseServer; let mongoClient; +let proxyProcess; +let proxyServerCleanup; + +/** + * Start MongoDB proxy with artificial latency + */ +async function startProxy() { + const { spawn } = require('child_process'); + + proxyProcess = spawn('node', ['benchmark/db-proxy.js'], { + env: { ...process.env, PROXY_PORT: '27018', TARGET_PORT: '27017', LATENCY_MS: '10000' }, + stdio: 'inherit', + }); + + // Wait for proxy to start + await new Promise(resolve => setTimeout(resolve, 2000)); + console.log('MongoDB proxy started on port 27018 with 10ms latency'); +} + +/** + * Stop MongoDB proxy + */ +async function stopProxy() { + if (proxyProcess) { + proxyProcess.kill(); + await new Promise(resolve => setTimeout(resolve, 500)); + console.log('MongoDB proxy stopped'); + } +} /** * Initialize Parse Server for benchmarking @@ -86,6 +115,66 @@ async function cleanupDatabase() { } } +/** + * Reset Parse SDK to use the default server + */ +function resetParseServer() { + Parse.serverURL = SERVER_URL; +} + +/** + * Start a Parse Server instance using the DB proxy for latency simulation + * Stores cleanup function globally for later use + */ +async function useProxyServer() { + const express = require('express'); + const { default: ParseServer } = require('../lib/index.js'); + + // Create a new Parse Server instance using the proxy + const app = express(); + const proxyParseServer = new ParseServer({ + databaseURI: 'mongodb://localhost:27018/parse_benchmark_test', + appId: APP_ID, + masterKey: MASTER_KEY, + serverURL: 'http://localhost:1338/parse', + silent: true, + allowClientClassCreation: true, + logLevel: 'error', + verbose: false, + }); + + app.use('/parse', proxyParseServer.app); + + const server = await new Promise((resolve, reject) => { + const s = app.listen(1338, (err) => { + if (err) { + reject(err); + } else { + resolve(s); + } + }); + }); + + // Configure Parse SDK to use the proxy server + Parse.serverURL = 'http://localhost:1338/parse'; + + // Store cleanup function globally + proxyServerCleanup = async () => { + server.close(); + await new Promise(resolve => setTimeout(resolve, 500)); + proxyServerCleanup = null; + }; +} + +/** + * Clean up proxy server if it's running + */ +async function cleanupProxyServer() { + if (proxyServerCleanup) { + await proxyServerCleanup(); + } +} + /** * Measure average time for an async operation over multiple iterations * Uses warmup iterations, median metric, and outlier filtering for robustness @@ -295,8 +384,12 @@ async function benchmarkUserLogin() { /** * Benchmark: Query with Include (Parallel Include Pointers) + * This test uses the TCP proxy (port 27018) to simulate 10ms database latency for more realistic measurements */ async function benchmarkQueryWithInclude() { + // Start proxy server + await useProxyServer(); + // Setup: Create nested object hierarchy const Level2Class = Parse.Object.extend('Level2'); const Level1Class = Parse.Object.extend('Level1'); @@ -332,11 +425,13 @@ async function benchmarkQueryWithInclude() { } await Parse.Object.saveAll(rootObjects); - return measureOperation('Query with Include (2 levels)', async () => { + const result = await 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 + }); + + return result; } /** @@ -349,6 +444,9 @@ async function runBenchmarks() { let server; try { + // Start MongoDB proxy + await startProxy(); + // Initialize Parse Server console.log('Initializing Parse Server...'); server = await initializeParseServer(); @@ -358,38 +456,26 @@ async function runBenchmarks() { const results = []; - // Run each benchmark with database cleanup - console.log('Running Object Create benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkObjectCreate()); - - console.log('Running Object Read benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkObjectRead()); + // 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 }, + ]; - console.log('Running Object Update benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkObjectUpdate()); - - console.log('Running Simple Query benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkSimpleQuery()); - - console.log('Running Batch Save benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkBatchSave()); - - console.log('Running User Signup benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkUserSignup()); - - console.log('Running User Login benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkUserLogin()); - - console.log('Running Query with Include benchmark...'); - await cleanupDatabase(); - results.push(await benchmarkQueryWithInclude()); + // Run each benchmark with database cleanup + for (const benchmark of benchmarks) { + console.log(`Running ${benchmark.name} benchmark...`); + resetParseServer(); + await cleanupDatabase(); + results.push(await benchmark.fn()); + await cleanupProxyServer(); + } // Output results in github-action-benchmark format (stdout) console.log(JSON.stringify(results, null, 2)); @@ -412,8 +498,9 @@ async function runBenchmarks() { if (server) { server.close(); } + await stopProxy(); // Give some time for cleanup - setTimeout(() => process.exit(0), 10000); + setTimeout(() => process.exit(0), 1000); } } From 713a4d009c9eaa97a615c5738bec569ef73f93aa Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:14:06 +0100 Subject: [PATCH 09/11] remove proxy --- benchmark/db-proxy.js | 70 ------------------------------ benchmark/performance.js | 94 +--------------------------------------- 2 files changed, 1 insertion(+), 163 deletions(-) delete mode 100644 benchmark/db-proxy.js diff --git a/benchmark/db-proxy.js b/benchmark/db-proxy.js deleted file mode 100644 index bdae86bc69..0000000000 --- a/benchmark/db-proxy.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Simple TCP proxy to add artificial latency to MongoDB connections - * This helps make benchmark measurements more stable by simulating network conditions - */ - -/* eslint-disable no-console */ - -const net = require('net'); - -const PROXY_PORT = parseInt(process.env.PROXY_PORT || '27018', 10); -const TARGET_HOST = process.env.TARGET_HOST || 'localhost'; -const TARGET_PORT = parseInt(process.env.TARGET_PORT || '27017', 10); -const LATENCY_MS = parseInt(process.env.LATENCY_MS || '10', 10); - -const server = net.createServer((clientSocket) => { - const serverSocket = net.createConnection({ - host: TARGET_HOST, - port: TARGET_PORT, - }); - - // Add latency to data flowing from client to MongoDB - clientSocket.on('data', (data) => { - setTimeout(() => { - if (!serverSocket.destroyed) { - serverSocket.write(data); - } - }, LATENCY_MS); - }); - - // Add latency to data flowing from MongoDB to client - serverSocket.on('data', (data) => { - setTimeout(() => { - if (!clientSocket.destroyed) { - clientSocket.write(data); - } - }, LATENCY_MS); - }); - - clientSocket.on('error', () => { - serverSocket.destroy(); - }); - - serverSocket.on('error', () => { - clientSocket.destroy(); - }); - - clientSocket.on('close', () => { - serverSocket.destroy(); - }); - - serverSocket.on('close', () => { - clientSocket.destroy(); - }); -}); - -server.listen(PROXY_PORT, () => { - console.log(`MongoDB proxy listening on port ${PROXY_PORT} forwarding to ${TARGET_PORT} with ${LATENCY_MS}ms latency`); -}); - -process.on('SIGTERM', () => { - server.close(() => { - process.exit(0); - }); -}); - -process.on('SIGINT', () => { - server.close(() => { - process.exit(0); - }); -}); diff --git a/benchmark/performance.js b/benchmark/performance.js index dd2305f676..511355b488 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -19,40 +19,11 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_ const SERVER_URL = 'http://localhost:1337/parse'; const APP_ID = 'benchmark-app-id'; const MASTER_KEY = 'benchmark-master-key'; -const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '10000', 10); +const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '1000', 10); // Parse Server instance let parseServer; let mongoClient; -let proxyProcess; -let proxyServerCleanup; - -/** - * Start MongoDB proxy with artificial latency - */ -async function startProxy() { - const { spawn } = require('child_process'); - - proxyProcess = spawn('node', ['benchmark/db-proxy.js'], { - env: { ...process.env, PROXY_PORT: '27018', TARGET_PORT: '27017', LATENCY_MS: '10000' }, - stdio: 'inherit', - }); - - // Wait for proxy to start - await new Promise(resolve => setTimeout(resolve, 2000)); - console.log('MongoDB proxy started on port 27018 with 10ms latency'); -} - -/** - * Stop MongoDB proxy - */ -async function stopProxy() { - if (proxyProcess) { - proxyProcess.kill(); - await new Promise(resolve => setTimeout(resolve, 500)); - console.log('MongoDB proxy stopped'); - } -} /** * Initialize Parse Server for benchmarking @@ -122,59 +93,6 @@ function resetParseServer() { Parse.serverURL = SERVER_URL; } -/** - * Start a Parse Server instance using the DB proxy for latency simulation - * Stores cleanup function globally for later use - */ -async function useProxyServer() { - const express = require('express'); - const { default: ParseServer } = require('../lib/index.js'); - - // Create a new Parse Server instance using the proxy - const app = express(); - const proxyParseServer = new ParseServer({ - databaseURI: 'mongodb://localhost:27018/parse_benchmark_test', - appId: APP_ID, - masterKey: MASTER_KEY, - serverURL: 'http://localhost:1338/parse', - silent: true, - allowClientClassCreation: true, - logLevel: 'error', - verbose: false, - }); - - app.use('/parse', proxyParseServer.app); - - const server = await new Promise((resolve, reject) => { - const s = app.listen(1338, (err) => { - if (err) { - reject(err); - } else { - resolve(s); - } - }); - }); - - // Configure Parse SDK to use the proxy server - Parse.serverURL = 'http://localhost:1338/parse'; - - // Store cleanup function globally - proxyServerCleanup = async () => { - server.close(); - await new Promise(resolve => setTimeout(resolve, 500)); - proxyServerCleanup = null; - }; -} - -/** - * Clean up proxy server if it's running - */ -async function cleanupProxyServer() { - if (proxyServerCleanup) { - await proxyServerCleanup(); - } -} - /** * Measure average time for an async operation over multiple iterations * Uses warmup iterations, median metric, and outlier filtering for robustness @@ -384,11 +302,8 @@ async function benchmarkUserLogin() { /** * Benchmark: Query with Include (Parallel Include Pointers) - * This test uses the TCP proxy (port 27018) to simulate 10ms database latency for more realistic measurements */ async function benchmarkQueryWithInclude() { - // Start proxy server - await useProxyServer(); // Setup: Create nested object hierarchy const Level2Class = Parse.Object.extend('Level2'); @@ -444,9 +359,6 @@ async function runBenchmarks() { let server; try { - // Start MongoDB proxy - await startProxy(); - // Initialize Parse Server console.log('Initializing Parse Server...'); server = await initializeParseServer(); @@ -474,7 +386,6 @@ async function runBenchmarks() { resetParseServer(); await cleanupDatabase(); results.push(await benchmark.fn()); - await cleanupProxyServer(); } // Output results in github-action-benchmark format (stdout) @@ -498,9 +409,6 @@ async function runBenchmarks() { if (server) { server.close(); } - await stopProxy(); - // Give some time for cleanup - setTimeout(() => process.exit(0), 1000); } } From 0e17c8fe2d5fe884e2f7ef7784798c6c026485d4 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:18:07 +0100 Subject: [PATCH 10/11] fix --- benchmark/performance.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/benchmark/performance.js b/benchmark/performance.js index 511355b488..1335e08594 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -97,21 +97,28 @@ function resetParseServer() { * Measure average time for an async operation over multiple iterations * Uses warmup iterations, median metric, and outlier filtering for robustness */ -async function measureOperation(name, operation, iterations = ITERATIONS) { - const warmupCount = Math.floor(iterations * 0.2); // 20% warmup iterations +async function measureOperation(name, operation, iterations = ITERATIONS, skipWarmup = false) { + const warmupCount = skipWarmup ? 0 : Math.floor(iterations * 0.2); // 20% warmup iterations const times = []; - // Warmup phase - stabilize JIT compilation and caches - for (let i = 0; i < warmupCount; i++) { - await operation(); + if (warmupCount > 0) { + console.log(`Starting warmup phase (${warmupCount} iterations)...`); + const warmupStart = performance.now(); + for (let i = 0; i < warmupCount; i++) { + await operation(); + } + console.log(`Warmup took: ${(performance.now() - warmupStart).toFixed(2)}ms`); } // Measurement phase + console.log(`Starting measurement phase (${iterations} iterations)...`); for (let i = 0; i < iterations; i++) { const start = performance.now(); await operation(); const end = performance.now(); - times.push(end - start); + const duration = end - start; + times.push(duration); + console.log(`Iteration ${i + 1}: ${duration.toFixed(2)}ms`); } // Sort times for percentile calculations @@ -409,6 +416,8 @@ async function runBenchmarks() { if (server) { server.close(); } + // Give some time for cleanup + setTimeout(() => process.exit(0), 1000); } } From 96e8f1ff50d0dd973c2c0279b8b9d8ef7a929974 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:43:55 +0100 Subject: [PATCH 11/11] logging --- benchmark/performance.js | 245 +++++++++++++++++++++++---------------- 1 file changed, 145 insertions(+), 100 deletions(-) diff --git a/benchmark/performance.js b/benchmark/performance.js index 1335e08594..93ec6e3501 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -10,6 +10,7 @@ /* eslint-disable no-console */ +const core = require('@actions/core'); const Parse = require('parse/node'); const { performance, PerformanceObserver } = require('perf_hooks'); const { MongoClient } = require('mongodb'); @@ -20,11 +21,16 @@ const SERVER_URL = 'http://localhost:1337/parse'; const APP_ID = 'benchmark-app-id'; const MASTER_KEY = 'benchmark-master-key'; const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '1000', 10); +const LOG_ITERATIONS = false; // Parse Server instance let parseServer; let mongoClient; +// Logging helpers +const logInfo = message => core.info(message); +const logError = message => core.error(message); + /** * Initialize Parse Server for benchmarking */ @@ -94,33 +100,49 @@ function resetParseServer() { } /** - * Measure average time for an async operation over multiple iterations - * Uses warmup iterations, median metric, and outlier filtering for robustness + * Measure average time for an async operation over multiple iterations. + * @param {Object} options - Measurement options + * @param {string} options.name - Name of the operation being measured + * @param {Function} options.operation - Async function to measure + * @param {number} [options.iterations=ITERATIONS] - Number of iterations to run + * @param {boolean} [options.skipWarmup=false] - Skip warmup phase */ -async function measureOperation(name, operation, iterations = ITERATIONS, skipWarmup = false) { - const warmupCount = skipWarmup ? 0 : Math.floor(iterations * 0.2); // 20% warmup iterations +async function measureOperation({ name, operation, iterations = ITERATIONS, skipWarmup = false }) { + const warmupCount = skipWarmup ? 0 : Math.floor(iterations * 0.2); const times = []; if (warmupCount > 0) { - console.log(`Starting warmup phase (${warmupCount} iterations)...`); + logInfo(`Starting warmup phase of ${warmupCount} iterations...`); const warmupStart = performance.now(); for (let i = 0; i < warmupCount; i++) { await operation(); } - console.log(`Warmup took: ${(performance.now() - warmupStart).toFixed(2)}ms`); + logInfo(`Warmup took: ${(performance.now() - warmupStart).toFixed(2)}ms`); } // Measurement phase - console.log(`Starting measurement phase (${iterations} iterations)...`); + logInfo(`Starting measurement phase of ${iterations} iterations...`); + const progressInterval = Math.ceil(iterations / 10); // Log every 10% + const measurementStart = performance.now(); + for (let i = 0; i < iterations; i++) { const start = performance.now(); await operation(); const end = performance.now(); const duration = end - start; times.push(duration); - console.log(`Iteration ${i + 1}: ${duration.toFixed(2)}ms`); + + // Log progress every 10% or individual iterations if LOG_ITERATIONS is enabled + if (LOG_ITERATIONS) { + logInfo(`Iteration ${i + 1}: ${duration.toFixed(2)}ms`); + } else if ((i + 1) % progressInterval === 0 || i + 1 === iterations) { + const progress = Math.round(((i + 1) / iterations) * 100); + logInfo(`Progress: ${progress}%`); + } } + logInfo(`Measurement took: ${(performance.now() - measurementStart).toFixed(2)}ms`); + // Sort times for percentile calculations times.sort((a, b) => a - b); @@ -157,13 +179,16 @@ async function measureOperation(name, operation, iterations = ITERATIONS, skipWa async function benchmarkObjectCreate() { let counter = 0; - return measureOperation('Object Create', async () => { - const TestObject = Parse.Object.extend('BenchmarkTest'); - const obj = new TestObject(); - obj.set('testField', `test-value-${counter++}`); - obj.set('number', counter); - obj.set('boolean', true); - await obj.save(); + return measureOperation({ + name: 'Object Create', + operation: async () => { + const TestObject = Parse.Object.extend('BenchmarkTest'); + const obj = new TestObject(); + obj.set('testField', `test-value-${counter++}`); + obj.set('number', counter); + obj.set('boolean', true); + await obj.save(); + }, }); } @@ -185,9 +210,12 @@ async function benchmarkObjectRead() { let counter = 0; - return measureOperation('Object Read', async () => { - const query = new Parse.Query('BenchmarkTest'); - await query.get(objects[counter++ % objects.length].id); + return measureOperation({ + name: 'Object Read', + operation: async () => { + const query = new Parse.Query('BenchmarkTest'); + await query.get(objects[counter++ % objects.length].id); + }, }); } @@ -210,11 +238,14 @@ async function benchmarkObjectUpdate() { let counter = 0; - return measureOperation('Object Update', async () => { - const obj = objects[counter++ % objects.length]; - obj.increment('counter'); - obj.set('lastUpdated', new Date()); - await obj.save(); + return measureOperation({ + name: 'Object Update', + operation: async () => { + const obj = objects[counter++ % objects.length]; + obj.increment('counter'); + obj.set('lastUpdated', new Date()); + await obj.save(); + }, }); } @@ -237,10 +268,13 @@ async function benchmarkSimpleQuery() { let counter = 0; - return measureOperation('Simple Query', async () => { - const query = new Parse.Query('BenchmarkTest'); - query.equalTo('category', counter++ % 10); - await query.find(); + return measureOperation({ + name: 'Simple Query', + operation: async () => { + const query = new Parse.Query('BenchmarkTest'); + query.equalTo('category', counter++ % 10); + await query.find(); + }, }); } @@ -250,18 +284,21 @@ async function benchmarkSimpleQuery() { async function benchmarkBatchSave() { const BATCH_SIZE = 10; - return measureOperation('Batch Save (10 objects)', async () => { - const TestObject = Parse.Object.extend('BenchmarkTest'); - const objects = []; - - for (let i = 0; i < BATCH_SIZE; i++) { - const obj = new TestObject(); - obj.set('batchField', `batch-${i}`); - obj.set('timestamp', new Date()); - objects.push(obj); - } + return measureOperation({ + name: 'Batch Save (10 objects)', + operation: async () => { + const TestObject = Parse.Object.extend('BenchmarkTest'); + const objects = []; + + for (let i = 0; i < BATCH_SIZE; i++) { + const obj = new TestObject(); + obj.set('batchField', `batch-${i}`); + obj.set('timestamp', new Date()); + objects.push(obj); + } - await Parse.Object.saveAll(objects); + await Parse.Object.saveAll(objects); + }, }); } @@ -271,13 +308,16 @@ async function benchmarkBatchSave() { async function benchmarkUserSignup() { let counter = 0; - return measureOperation('User Signup', async () => { - counter++; - const user = new Parse.User(); - user.set('username', `benchmark_user_${Date.now()}_${counter}`); - user.set('password', 'benchmark_password'); - user.set('email', `benchmark${counter}@example.com`); - await user.signUp(); + return measureOperation({ + name: 'User Signup', + operation: async () => { + counter++; + const user = new Parse.User(); + user.set('username', `benchmark_user_${Date.now()}_${counter}`); + user.set('password', 'benchmark_password'); + user.set('email', `benchmark${counter}@example.com`); + await user.signUp(); + }, }); } @@ -300,10 +340,13 @@ async function benchmarkUserLogin() { let counter = 0; - return measureOperation('User Login', async () => { - const userCreds = users[counter++ % users.length]; - await Parse.User.logIn(userCreds.username, userCreds.password); - await Parse.User.logOut(); + return measureOperation({ + name: 'User Login', + operation: async () => { + const userCreds = users[counter++ % users.length]; + await Parse.User.logIn(userCreds.username, userCreds.password); + await Parse.User.logOut(); + }, }); } @@ -317,57 +360,59 @@ async function benchmarkQueryWithInclude() { 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({ + name: 'Query with Include (2 levels)', + skipWarmup: true, + 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 result = await measureOperation('Query with Include (2 levels)', async () => { - const query = new Parse.Query('Root'); - query.include('level1.level2'); - await query.find(); + const query = new Parse.Query('Root'); + query.include('level1.level2'); + await query.find(); + }, }); - - return result; } /** * Run all benchmarks */ async function runBenchmarks() { - console.log('Starting Parse Server Performance Benchmarks...'); - console.log(`Iterations per benchmark: ${ITERATIONS}`); + logInfo('Starting Parse Server Performance Benchmarks...'); + logInfo(`Iterations per benchmark: ${ITERATIONS}`); let server; try { // Initialize Parse Server - console.log('Initializing Parse Server...'); + logInfo('Initializing Parse Server...'); server = await initializeParseServer(); // Wait for server to be ready @@ -377,36 +422,36 @@ 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: '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 }, ]; // Run each benchmark with database cleanup for (const benchmark of benchmarks) { - console.log(`Running ${benchmark.name} benchmark...`); + logInfo(`\nRunning benchmark '${benchmark.name}'...`); resetParseServer(); await cleanupDatabase(); results.push(await benchmark.fn()); } // Output results in github-action-benchmark format (stdout) - console.log(JSON.stringify(results, null, 2)); + logInfo(JSON.stringify(results, null, 2)); // Output summary to stderr for visibility - console.log('Benchmarks completed successfully!'); - console.log('Summary:'); + logInfo('Benchmarks completed successfully!'); + logInfo('Summary:'); results.forEach(result => { - console.log(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`); + logInfo(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`); }); } catch (error) { - console.error('Error running benchmarks:', error); + logError('Error running benchmarks:', error); process.exit(1); } finally { // Cleanup