ci: Fix performance step in CI (#9921)

This commit is contained in:
Manuel
2025-11-09 02:02:17 +01:00
committed by GitHub
parent a85ba199be
commit b73ebac5c9
3 changed files with 87 additions and 41 deletions

View File

@@ -27,11 +27,31 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- name: Checkout PR branch (for benchmark script)
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 1
- name: Save PR benchmark script
run: |
mkdir -p /tmp/pr-benchmark
cp -r benchmark /tmp/pr-benchmark/ || echo "No benchmark directory"
cp package.json /tmp/pr-benchmark/ || true
- name: Checkout base branch - name: Checkout base branch
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}
fetch-depth: 1 fetch-depth: 1
clean: true
- name: Restore PR benchmark script
run: |
if [ -d "/tmp/pr-benchmark/benchmark" ]; then
rm -rf benchmark
cp -r /tmp/pr-benchmark/benchmark .
fi
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -47,17 +67,18 @@ jobs:
- name: Run baseline benchmarks - name: Run baseline benchmarks
id: baseline id: baseline
env:
NODE_ENV: production
run: | run: |
echo "Checking if benchmark script exists..." echo "Running baseline benchmarks with CPU affinity (using PR's benchmark script)..."
if [ ! -f "benchmark/performance.js" ]; then if [ ! -f "benchmark/performance.js" ]; then
echo "⚠️ Benchmark script not found in base branch - this is expected for new features" echo "⚠️ Benchmark script not found - this is expected for new features"
echo "Skipping baseline benchmark" echo "Skipping baseline benchmark"
echo '[]' > baseline.json echo '[]' > baseline.json
echo "Baseline: N/A (benchmark script not in base branch)" > baseline-output.txt echo "Baseline: N/A (no benchmark script)" > baseline-output.txt
exit 0 exit 0
fi fi
echo "Running baseline benchmarks..." taskset -c 0 npm run benchmark > baseline-output.txt 2>&1 || npm run benchmark > baseline-output.txt 2>&1 || true
npm run benchmark > baseline-output.txt 2>&1 || true
echo "Benchmark command completed with exit code: $?" echo "Benchmark command completed with exit code: $?"
echo "Output file size: $(wc -c < baseline-output.txt) bytes" echo "Output file size: $(wc -c < baseline-output.txt) bytes"
echo "--- Begin baseline-output.txt ---" echo "--- Begin baseline-output.txt ---"
@@ -111,9 +132,11 @@ jobs:
- name: Run PR benchmarks - name: Run PR benchmarks
id: pr-bench id: pr-bench
env:
NODE_ENV: production
run: | run: |
echo "Running PR benchmarks..." echo "Running PR benchmarks with CPU affinity..."
npm run benchmark > pr-output.txt 2>&1 || true 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 "Benchmark command completed with exit code: $?"
echo "Output file size: $(wc -c < pr-output.txt) bytes" echo "Output file size: $(wc -c < pr-output.txt) bytes"
echo "--- Begin pr-output.txt ---" echo "--- Begin pr-output.txt ---"
@@ -224,13 +247,13 @@ jobs:
const changeStr = change > 0 ? \`+\${change.toFixed(1)}%\` : \`\${change.toFixed(1)}%\`; const changeStr = change > 0 ? \`+\${change.toFixed(1)}%\` : \`\${change.toFixed(1)}%\`;
let status = '✅'; let status = '✅';
if (change > 20) { if (change > 100) {
status = '❌ Much Slower'; status = '❌ Much Slower';
hasRegression = true; hasRegression = true;
} else if (change > 10) { } else if (change > 50) {
status = '⚠️ Slower'; status = '⚠️ Slower';
hasRegression = true; hasRegression = true;
} else if (change < -10) { } else if (change < -50) {
status = '🚀 Faster'; status = '🚀 Faster';
hasImprovement = true; hasImprovement = true;
} }
@@ -281,7 +304,9 @@ jobs:
echo "" >> comment.md echo "" >> comment.md
echo "</details>" >> comment.md echo "</details>" >> comment.md
echo "" >> comment.md echo "" >> comment.md
echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-100} iterations per test on Node.js ${{ env.NODE_VERSION }}*" >> comment.md echo "*Benchmarks ran with ${BENCHMARK_ITERATIONS:-10000} iterations per test on Node.js ${{ env.NODE_VERSION }} (production mode, CPU pinned)*" >> comment.md
echo "" >> comment.md
echo "> **Note:** Using 10k iterations with CPU affinity for measurement stability. Thresholds: ⚠️ >50%, ❌ >100%." >> comment.md
- name: Comment PR with results - name: Comment PR with results
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'

View File

@@ -341,7 +341,7 @@ Performance benchmarks are located in [`benchmark/performance.js`](benchmark/per
4. **Test locally**: Run the benchmarks locally to verify they work: 4. **Test locally**: Run the benchmarks locally to verify they work:
```bash ```bash
npm run benchmark:quick # Quick test with 10 iterations npm run benchmark:quick # Quick test with 10 iterations
npm run benchmark # Full test with 100 iterations npm run benchmark # Full test with 10,000 iterations
``` ```
For new features where no baseline exists, the CI will establish new benchmarks that future PRs will be compared against. For new features where no baseline exists, the CI will establish new benchmarks that future PRs will be compared against.

View File

@@ -8,6 +8,8 @@
* Run with: npm run benchmark * Run with: npm run benchmark
*/ */
/* eslint-disable no-console */
const Parse = require('parse/node'); const Parse = require('parse/node');
const { performance, PerformanceObserver } = require('perf_hooks'); const { performance, PerformanceObserver } = require('perf_hooks');
const { MongoClient } = require('mongodb'); const { MongoClient } = require('mongodb');
@@ -17,7 +19,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_
const SERVER_URL = 'http://localhost:1337/parse'; const SERVER_URL = 'http://localhost:1337/parse';
const APP_ID = 'benchmark-app-id'; const APP_ID = 'benchmark-app-id';
const MASTER_KEY = 'benchmark-master-key'; const MASTER_KEY = 'benchmark-master-key';
const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '100', 10); const ITERATIONS = parseInt(process.env.BENCHMARK_ITERATIONS || '10000', 10);
// Parse Server instance // Parse Server instance
let parseServer; let parseServer;
@@ -39,6 +41,8 @@ async function initializeParseServer() {
serverURL: SERVER_URL, serverURL: SERVER_URL,
silent: true, silent: true,
allowClientClassCreation: true, allowClientClassCreation: true,
logLevel: 'error', // Minimal logging for performance
verbose: false,
}); });
app.use('/parse', parseServer.app); app.use('/parse', parseServer.app);
@@ -84,10 +88,18 @@ async function cleanupDatabase() {
/** /**
* Measure average time for an async operation over multiple iterations * 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) { async function measureOperation(name, operation, iterations = ITERATIONS) {
const warmupCount = Math.floor(iterations * 0.2); // 20% warmup iterations
const times = []; const times = [];
// Warmup phase - stabilize JIT compilation and caches
for (let i = 0; i < warmupCount; i++) {
await operation();
}
// Measurement phase
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
const start = performance.now(); const start = performance.now();
await operation(); await operation();
@@ -95,22 +107,33 @@ async function measureOperation(name, operation, iterations = ITERATIONS) {
times.push(end - start); times.push(end - start);
} }
// Calculate statistics // Sort times for percentile calculations
times.sort((a, b) => a - b); times.sort((a, b) => a - b);
const sum = times.reduce((acc, val) => acc + val, 0);
const mean = sum / times.length; // Filter outliers using Interquartile Range (IQR) method
const p50 = times[Math.floor(times.length * 0.5)]; const q1Index = Math.floor(times.length * 0.25);
const p95 = times[Math.floor(times.length * 0.95)]; const q3Index = Math.floor(times.length * 0.75);
const p99 = times[Math.floor(times.length * 0.99)]; const q1 = times[q1Index];
const min = times[0]; const q3 = times[q3Index];
const max = times[times.length - 1]; const iqr = q3 - q1;
const lowerBound = q1 - 1.5 * iqr;
const upperBound = q3 + 1.5 * iqr;
const filtered = times.filter(t => t >= lowerBound && t <= upperBound);
// Calculate statistics on filtered data
const median = filtered[Math.floor(filtered.length * 0.5)];
const p95 = filtered[Math.floor(filtered.length * 0.95)];
const p99 = filtered[Math.floor(filtered.length * 0.99)];
const min = filtered[0];
const max = filtered[filtered.length - 1];
return { return {
name, name,
value: mean, value: median, // Use median (p50) as primary metric for stability in CI
unit: 'ms', unit: 'ms',
range: `${min.toFixed(2)} - ${max.toFixed(2)}`, range: `${min.toFixed(2)} - ${max.toFixed(2)}`,
extra: `p50: ${p50.toFixed(2)}ms, p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms`, extra: `p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms, n=${filtered.length}/${times.length}`,
}; };
} }
@@ -274,15 +297,14 @@ async function benchmarkUserLogin() {
* Run all benchmarks * Run all benchmarks
*/ */
async function runBenchmarks() { async function runBenchmarks() {
console.error('Starting Parse Server Performance Benchmarks...'); console.log('Starting Parse Server Performance Benchmarks...');
console.error(`Iterations per benchmark: ${ITERATIONS}`); console.log(`Iterations per benchmark: ${ITERATIONS}`);
console.error('');
let server; let server;
try { try {
// Initialize Parse Server // Initialize Parse Server
console.error('Initializing Parse Server...'); console.log('Initializing Parse Server...');
server = await initializeParseServer(); server = await initializeParseServer();
// Wait for server to be ready // Wait for server to be ready
@@ -291,43 +313,42 @@ async function runBenchmarks() {
const results = []; const results = [];
// Run each benchmark with database cleanup // Run each benchmark with database cleanup
console.error('Running Object Create benchmark...'); console.log('Running Object Create benchmark...');
await cleanupDatabase(); await cleanupDatabase();
results.push(await benchmarkObjectCreate()); results.push(await benchmarkObjectCreate());
console.error('Running Object Read benchmark...'); console.log('Running Object Read benchmark...');
await cleanupDatabase(); await cleanupDatabase();
results.push(await benchmarkObjectRead()); results.push(await benchmarkObjectRead());
console.error('Running Object Update benchmark...'); console.log('Running Object Update benchmark...');
await cleanupDatabase(); await cleanupDatabase();
results.push(await benchmarkObjectUpdate()); results.push(await benchmarkObjectUpdate());
console.error('Running Simple Query benchmark...'); console.log('Running Simple Query benchmark...');
await cleanupDatabase(); await cleanupDatabase();
results.push(await benchmarkSimpleQuery()); results.push(await benchmarkSimpleQuery());
console.error('Running Batch Save benchmark...'); console.log('Running Batch Save benchmark...');
await cleanupDatabase(); await cleanupDatabase();
results.push(await benchmarkBatchSave()); results.push(await benchmarkBatchSave());
console.error('Running User Signup benchmark...'); console.log('Running User Signup benchmark...');
await cleanupDatabase(); await cleanupDatabase();
results.push(await benchmarkUserSignup()); results.push(await benchmarkUserSignup());
console.error('Running User Login benchmark...'); console.log('Running User Login benchmark...');
await cleanupDatabase(); await cleanupDatabase();
results.push(await benchmarkUserLogin()); results.push(await benchmarkUserLogin());
// Output results in github-action-benchmark format // Output results in github-action-benchmark format (stdout)
console.log(JSON.stringify(results, null, 2)); console.log(JSON.stringify(results, null, 2));
console.error(''); // Output summary to stderr for visibility
console.error('Benchmarks completed successfully!'); console.log('Benchmarks completed successfully!');
console.error(''); console.log('Summary:');
console.error('Summary:');
results.forEach(result => { results.forEach(result => {
console.error(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`); console.log(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`);
}); });
} catch (error) { } catch (error) {
@@ -342,7 +363,7 @@ async function runBenchmarks() {
server.close(); server.close();
} }
// Give some time for cleanup // Give some time for cleanup
setTimeout(() => process.exit(0), 1000); setTimeout(() => process.exit(0), 10000);
} }
} }