Skip to content

Commit ffcae78

Browse files
committed
Merge branch 'alpha' of github.com:parse-community/parse-server into moumouls/upgrade-appollo-server
# Conflicts: # src/GraphQL/ParseGraphQLServer.js
2 parents b259832 + 50650a3 commit ffcae78

File tree

17 files changed

+870
-180
lines changed

17 files changed

+870
-180
lines changed

.github/workflows/ci-performance.yml

Lines changed: 36 additions & 10 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..."
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: |
115138
echo "Running PR benchmarks..."
116-
npm run benchmark > pr-output.txt 2>&1 || true
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/MongoLatencyWrapper.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* MongoDB Latency Wrapper
3+
*
4+
* Utility to inject artificial latency into MongoDB operations for performance testing.
5+
* This wrapper temporarily wraps MongoDB Collection methods to add delays before
6+
* database operations execute.
7+
*
8+
* Usage:
9+
* const { wrapMongoDBWithLatency } = require('./MongoLatencyWrapper');
10+
*
11+
* // Before initializing Parse Server
12+
* const unwrap = wrapMongoDBWithLatency(10); // 10ms delay
13+
*
14+
* // ... run benchmarks ...
15+
*
16+
* // Cleanup when done
17+
* unwrap();
18+
*/
19+
20+
const { Collection } = require('mongodb');
21+
22+
// Store original methods for restoration
23+
const originalMethods = new Map();
24+
25+
/**
26+
* Wrap a Collection method to add artificial latency
27+
* @param {string} methodName - Name of the method to wrap
28+
* @param {number} latencyMs - Delay in milliseconds
29+
*/
30+
function wrapMethod(methodName, latencyMs) {
31+
if (!originalMethods.has(methodName)) {
32+
originalMethods.set(methodName, Collection.prototype[methodName]);
33+
}
34+
35+
const originalMethod = originalMethods.get(methodName);
36+
37+
Collection.prototype[methodName] = function (...args) {
38+
// For methods that return cursors (like find, aggregate), we need to delay the execution
39+
// but still return a cursor-like object
40+
const result = originalMethod.apply(this, args);
41+
42+
// Check if result has cursor methods (toArray, forEach, etc.)
43+
if (result && typeof result.toArray === 'function') {
44+
// Wrap cursor methods that actually execute the query
45+
const originalToArray = result.toArray.bind(result);
46+
result.toArray = function() {
47+
// Wait for the original promise to settle, then delay the result
48+
return originalToArray().then(
49+
value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)),
50+
error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs))
51+
);
52+
};
53+
return result;
54+
}
55+
56+
// For promise-returning methods, wrap the promise with delay
57+
if (result && typeof result.then === 'function') {
58+
// Wait for the original promise to settle, then delay the result
59+
return result.then(
60+
value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)),
61+
error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs))
62+
);
63+
}
64+
65+
// For synchronous methods, just add delay
66+
return new Promise((resolve) => {
67+
setTimeout(() => {
68+
resolve(result);
69+
}, latencyMs);
70+
});
71+
};
72+
}
73+
74+
/**
75+
* Wrap MongoDB Collection methods with artificial latency
76+
* @param {number} latencyMs - Delay in milliseconds to inject before each operation
77+
* @returns {Function} unwrap - Function to restore original methods
78+
*/
79+
function wrapMongoDBWithLatency(latencyMs) {
80+
if (typeof latencyMs !== 'number' || latencyMs < 0) {
81+
throw new Error('latencyMs must be a non-negative number');
82+
}
83+
84+
if (latencyMs === 0) {
85+
// eslint-disable-next-line no-console
86+
console.log('Latency is 0ms, skipping MongoDB wrapping');
87+
return () => {}; // No-op unwrap function
88+
}
89+
90+
// eslint-disable-next-line no-console
91+
console.log(`Wrapping MongoDB operations with ${latencyMs}ms artificial latency`);
92+
93+
// List of MongoDB Collection methods to wrap
94+
const methodsToWrap = [
95+
'find',
96+
'findOne',
97+
'countDocuments',
98+
'estimatedDocumentCount',
99+
'distinct',
100+
'aggregate',
101+
'insertOne',
102+
'insertMany',
103+
'updateOne',
104+
'updateMany',
105+
'replaceOne',
106+
'deleteOne',
107+
'deleteMany',
108+
'findOneAndUpdate',
109+
'findOneAndReplace',
110+
'findOneAndDelete',
111+
'createIndex',
112+
'createIndexes',
113+
'dropIndex',
114+
'dropIndexes',
115+
'drop',
116+
];
117+
118+
methodsToWrap.forEach(methodName => {
119+
wrapMethod(methodName, latencyMs);
120+
});
121+
122+
// Return unwrap function to restore original methods
123+
return function unwrap() {
124+
// eslint-disable-next-line no-console
125+
console.log('Removing MongoDB latency wrapper, restoring original methods');
126+
127+
originalMethods.forEach((originalMethod, methodName) => {
128+
Collection.prototype[methodName] = originalMethod;
129+
});
130+
131+
originalMethods.clear();
132+
};
133+
}
134+
135+
module.exports = {
136+
wrapMongoDBWithLatency,
137+
};

0 commit comments

Comments
 (0)