Skip to content

Commit 5d405e9

Browse files
committed
Merge branch 'moumouls/include-prevention-complexity' of github.com:Moumouls/parse-server into moumouls/include-prevention-complexity
2 parents 6d59d8f + 9343bc0 commit 5d405e9

File tree

3 files changed

+90
-43
lines changed

3 files changed

+90
-43
lines changed

.github/workflows/ci-performance.yml

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,31 @@ jobs:
2727
timeout-minutes: 30
2828

2929
steps:
30+
- name: Checkout PR branch (for benchmark script)
31+
uses: actions/checkout@v4
32+
with:
33+
ref: ${{ github.event.pull_request.head.sha }}
34+
fetch-depth: 1
35+
36+
- name: Save PR benchmark script
37+
run: |
38+
mkdir -p /tmp/pr-benchmark
39+
cp -r benchmark /tmp/pr-benchmark/ || echo "No benchmark directory"
40+
cp package.json /tmp/pr-benchmark/ || true
41+
3042
- name: Checkout base branch
3143
uses: actions/checkout@v4
3244
with:
3345
ref: ${{ github.base_ref }}
3446
fetch-depth: 1
47+
clean: true
48+
49+
- name: Restore PR benchmark script
50+
run: |
51+
if [ -d "/tmp/pr-benchmark/benchmark" ]; then
52+
rm -rf benchmark
53+
cp -r /tmp/pr-benchmark/benchmark .
54+
fi
3555
3656
- name: Setup Node.js
3757
uses: actions/setup-node@v4
@@ -47,17 +67,18 @@ jobs:
4767

4868
- name: Run baseline benchmarks
4969
id: baseline
70+
env:
71+
NODE_ENV: production
5072
run: |
51-
echo "Checking if benchmark script exists..."
73+
echo "Running baseline benchmarks with CPU affinity (using PR's benchmark script)..."
5274
if [ ! -f "benchmark/performance.js" ]; then
53-
echo "⚠️ Benchmark script not found in base branch - this is expected for new features"
75+
echo "⚠️ Benchmark script not found - this is expected for new features"
5476
echo "Skipping baseline benchmark"
5577
echo '[]' > baseline.json
56-
echo "Baseline: N/A (benchmark script not in base branch)" > baseline-output.txt
78+
echo "Baseline: N/A (no benchmark script)" > baseline-output.txt
5779
exit 0
5880
fi
59-
echo "Running baseline benchmarks..."
60-
npm run benchmark > baseline-output.txt 2>&1 || true
81+
taskset -c 0 npm run benchmark > baseline-output.txt 2>&1 || npm run benchmark > baseline-output.txt 2>&1 || true
6182
echo "Benchmark command completed with exit code: $?"
6283
echo "Output file size: $(wc -c < baseline-output.txt) bytes"
6384
echo "--- Begin baseline-output.txt ---"
@@ -111,9 +132,11 @@ jobs:
111132

112133
- name: Run PR benchmarks
113134
id: pr-bench
135+
env:
136+
NODE_ENV: production
114137
run: |
115-
echo "Running PR benchmarks..."
116-
npm run benchmark > pr-output.txt 2>&1 || true
138+
echo "Running PR benchmarks with CPU affinity..."
139+
taskset -c 0 npm run benchmark > pr-output.txt 2>&1 || npm run benchmark > pr-output.txt 2>&1 || true
117140
echo "Benchmark command completed with exit code: $?"
118141
echo "Output file size: $(wc -c < pr-output.txt) bytes"
119142
echo "--- Begin pr-output.txt ---"
@@ -224,13 +247,13 @@ jobs:
224247
const changeStr = change > 0 ? \`+\${change.toFixed(1)}%\` : \`\${change.toFixed(1)}%\`;
225248
226249
let status = '✅';
227-
if (change > 20) {
250+
if (change > 50) {
228251
status = '❌ Much Slower';
229252
hasRegression = true;
230-
} else if (change > 10) {
253+
} else if (change > 25) {
231254
status = '⚠️ Slower';
232255
hasRegression = true;
233-
} else if (change < -10) {
256+
} else if (change < -25) {
234257
status = '🚀 Faster';
235258
hasImprovement = true;
236259
}
@@ -281,7 +304,7 @@ jobs:
281304
echo "" >> comment.md
282305
echo "</details>" >> comment.md
283306
echo "" >> comment.md
284-
echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-100} iterations per test on Node.js ${{ env.NODE_VERSION }}*" >> comment.md
307+
echo "> **Note:** Thresholds: ⚠️ >25%, ❌ >50%." >> comment.md
285308
286309
- name: Comment PR with results
287310
if: github.event_name == 'pull_request'
@@ -300,3 +323,6 @@ jobs:
300323
else
301324
echo "⚠️ Benchmark comparison not available" >> $GITHUB_STEP_SUMMARY
302325
fi
326+
concurrency:
327+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
328+
cancel-in-progress: true

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ Performance benchmarks are located in [`benchmark/performance.js`](benchmark/per
341341
4. **Test locally**: Run the benchmarks locally to verify they work:
342342
```bash
343343
npm run benchmark:quick # Quick test with 10 iterations
344-
npm run benchmark # Full test with 100 iterations
344+
npm run benchmark # Full test with 10,000 iterations
345345
```
346346

347347
For new features where no baseline exists, the CI will establish new benchmarks that future PRs will be compared against.

benchmark/performance.js

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* Run with: npm run benchmark
99
*/
1010

11+
/* eslint-disable no-console */
12+
1113
const Parse = require('parse/node');
1214
const { performance, PerformanceObserver } = require('perf_hooks');
1315
const { MongoClient } = require('mongodb');
@@ -17,7 +19,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_
1719
const SERVER_URL = 'http://localhost:1337/parse';
1820
const APP_ID = 'benchmark-app-id';
1921
const MASTER_KEY = 'benchmark-master-key';
20-
const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '100', 10);
22+
const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '1000', 10);
2123

2224
// Parse Server instance
2325
let parseServer;
@@ -39,6 +41,8 @@ async function initializeParseServer() {
3941
serverURL: SERVER_URL,
4042
silent: true,
4143
allowClientClassCreation: true,
44+
logLevel: 'error', // Minimal logging for performance
45+
verbose: false,
4246
});
4347

4448
app.use('/parse', parseServer.app);
@@ -84,33 +88,52 @@ async function cleanupDatabase() {
8488

8589
/**
8690
* Measure average time for an async operation over multiple iterations
91+
* Uses warmup iterations, median metric, and outlier filtering for robustness
8792
*/
8893
async function measureOperation(name, operation, iterations = ITERATIONS) {
94+
const warmupCount = Math.floor(iterations * 0.2); // 20% warmup iterations
8995
const times = [];
9096

97+
// Warmup phase - stabilize JIT compilation and caches
98+
for (let i = 0; i < warmupCount; i++) {
99+
await operation();
100+
}
101+
102+
// Measurement phase
91103
for (let i = 0; i < iterations; i++) {
92104
const start = performance.now();
93105
await operation();
94106
const end = performance.now();
95107
times.push(end - start);
96108
}
97109

98-
// Calculate statistics
110+
// Sort times for percentile calculations
99111
times.sort((a, b) => a - b);
100-
const sum = times.reduce((acc, val) => acc + val, 0);
101-
const mean = sum / times.length;
102-
const p50 = times[Math.floor(times.length * 0.5)];
103-
const p95 = times[Math.floor(times.length * 0.95)];
104-
const p99 = times[Math.floor(times.length * 0.99)];
105-
const min = times[0];
106-
const max = times[times.length - 1];
112+
113+
// Filter outliers using Interquartile Range (IQR) method
114+
const q1Index = Math.floor(times.length * 0.25);
115+
const q3Index = Math.floor(times.length * 0.75);
116+
const q1 = times[q1Index];
117+
const q3 = times[q3Index];
118+
const iqr = q3 - q1;
119+
const lowerBound = q1 - 1.5 * iqr;
120+
const upperBound = q3 + 1.5 * iqr;
121+
122+
const filtered = times.filter(t => t >= lowerBound && t <= upperBound);
123+
124+
// Calculate statistics on filtered data
125+
const median = filtered[Math.floor(filtered.length * 0.5)];
126+
const p95 = filtered[Math.floor(filtered.length * 0.95)];
127+
const p99 = filtered[Math.floor(filtered.length * 0.99)];
128+
const min = filtered[0];
129+
const max = filtered[filtered.length - 1];
107130

108131
return {
109132
name,
110-
value: mean,
133+
value: median, // Use median (p50) as primary metric for stability in CI
111134
unit: 'ms',
112135
range: `${min.toFixed(2)} - ${max.toFixed(2)}`,
113-
extra: `p50: ${p50.toFixed(2)}ms, p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms`,
136+
extra: `p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms, n=${filtered.length}/${times.length}`,
114137
};
115138
}
116139

@@ -225,7 +248,7 @@ async function benchmarkBatchSave() {
225248
}
226249

227250
await Parse.Object.saveAll(objects);
228-
}, Math.floor(ITERATIONS / BATCH_SIZE)); // Fewer iterations for batch operations
251+
});
229252
}
230253

231254
/**
@@ -241,7 +264,7 @@ async function benchmarkUserSignup() {
241264
user.set('password', 'benchmark_password');
242265
user.set('email', `benchmark${counter}@example.com`);
243266
await user.signUp();
244-
}, Math.floor(ITERATIONS / 10)); // Fewer iterations for user operations
267+
});
245268
}
246269

247270
/**
@@ -267,22 +290,21 @@ async function benchmarkUserLogin() {
267290
const userCreds = users[counter++ % users.length];
268291
await Parse.User.logIn(userCreds.username, userCreds.password);
269292
await Parse.User.logOut();
270-
}, Math.floor(ITERATIONS / 10)); // Fewer iterations for user operations
293+
});
271294
}
272295

273296
/**
274297
* Run all benchmarks
275298
*/
276299
async function runBenchmarks() {
277-
console.error('Starting Parse Server Performance Benchmarks...');
278-
console.error(`Iterations per benchmark: ${ITERATIONS}`);
279-
console.error('');
300+
console.log('Starting Parse Server Performance Benchmarks...');
301+
console.log(`Iterations per benchmark: ${ITERATIONS}`);
280302

281303
let server;
282304

283305
try {
284306
// Initialize Parse Server
285-
console.error('Initializing Parse Server...');
307+
console.log('Initializing Parse Server...');
286308
server = await initializeParseServer();
287309

288310
// Wait for server to be ready
@@ -291,43 +313,42 @@ async function runBenchmarks() {
291313
const results = [];
292314

293315
// Run each benchmark with database cleanup
294-
console.error('Running Object Create benchmark...');
316+
console.log('Running Object Create benchmark...');
295317
await cleanupDatabase();
296318
results.push(await benchmarkObjectCreate());
297319

298-
console.error('Running Object Read benchmark...');
320+
console.log('Running Object Read benchmark...');
299321
await cleanupDatabase();
300322
results.push(await benchmarkObjectRead());
301323

302-
console.error('Running Object Update benchmark...');
324+
console.log('Running Object Update benchmark...');
303325
await cleanupDatabase();
304326
results.push(await benchmarkObjectUpdate());
305327

306-
console.error('Running Simple Query benchmark...');
328+
console.log('Running Simple Query benchmark...');
307329
await cleanupDatabase();
308330
results.push(await benchmarkSimpleQuery());
309331

310-
console.error('Running Batch Save benchmark...');
332+
console.log('Running Batch Save benchmark...');
311333
await cleanupDatabase();
312334
results.push(await benchmarkBatchSave());
313335

314-
console.error('Running User Signup benchmark...');
336+
console.log('Running User Signup benchmark...');
315337
await cleanupDatabase();
316338
results.push(await benchmarkUserSignup());
317339

318-
console.error('Running User Login benchmark...');
340+
console.log('Running User Login benchmark...');
319341
await cleanupDatabase();
320342
results.push(await benchmarkUserLogin());
321343

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

325-
console.error('');
326-
console.error('Benchmarks completed successfully!');
327-
console.error('');
328-
console.error('Summary:');
347+
// Output summary to stderr for visibility
348+
console.log('Benchmarks completed successfully!');
349+
console.log('Summary:');
329350
results.forEach(result => {
330-
console.error(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`);
351+
console.log(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`);
331352
});
332353

333354
} catch (error) {

0 commit comments

Comments
 (0)