Skip to content

Commit b344927

Browse files
authored
ci: Fix flaky GC tests (parse-community#10294)
1 parent c77330d commit b344927

3 files changed

Lines changed: 50 additions & 42 deletions

File tree

.github/workflows/ci-performance.yml

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
env:
6969
NODE_ENV: production
7070
run: |
71+
set -o pipefail
7172
echo "Running baseline benchmarks..."
7273
if [ ! -f "benchmark/performance.js" ]; then
7374
echo "⚠️ Benchmark script not found - this is expected for new features"
@@ -76,17 +77,9 @@ jobs:
7677
echo "Baseline: N/A (no benchmark script)" > baseline-output.txt
7778
exit 0
7879
fi
79-
taskset -c 0 npm run benchmark > baseline-output.txt 2>&1 || npm run benchmark > baseline-output.txt 2>&1 || true
80-
echo "Benchmark command completed with exit code: $?"
81-
echo "Output file size: $(wc -c < baseline-output.txt) bytes"
82-
echo "--- Begin baseline-output.txt ---"
83-
cat baseline-output.txt
84-
echo "--- End baseline-output.txt ---"
80+
taskset -c 0 npm run benchmark 2>&1 | tee baseline-output.txt || npm run benchmark 2>&1 | tee baseline-output.txt || true
8581
# Extract JSON from output (everything between first [ and last ])
8682
sed -n '/^\[/,/^\]/p' baseline-output.txt > baseline.json || echo '[]' > baseline.json
87-
echo "Extracted JSON size: $(wc -c < baseline.json) bytes"
88-
echo "Baseline benchmark results:"
89-
cat baseline.json
9083
continue-on-error: true
9184

9285
- name: Save baseline results to temp location
@@ -133,18 +126,11 @@ jobs:
133126
env:
134127
NODE_ENV: production
135128
run: |
129+
set -o pipefail
136130
echo "Running PR benchmarks..."
137-
taskset -c 0 npm run benchmark > pr-output.txt 2>&1 || npm run benchmark > pr-output.txt 2>&1 || true
138-
echo "Benchmark command completed with exit code: $?"
139-
echo "Output file size: $(wc -c < pr-output.txt) bytes"
140-
echo "--- Begin pr-output.txt ---"
141-
cat pr-output.txt
142-
echo "--- End pr-output.txt ---"
131+
taskset -c 0 npm run benchmark 2>&1 | tee pr-output.txt || npm run benchmark 2>&1 | tee pr-output.txt || true
143132
# Extract JSON from output (everything between first [ and last ])
144133
sed -n '/^\[/,/^\]/p' pr-output.txt > pr.json || echo '[]' > pr.json
145-
echo "Extracted JSON size: $(wc -c < pr.json) bytes"
146-
echo "PR benchmark results:"
147-
cat pr.json
148134
continue-on-error: true
149135

150136
- name: Upload PR results

benchmark/performance.js

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ let core;
3030
// Logging helpers
3131
const logInfo = message => core.info(message);
3232
const logError = message => core.error(message);
33+
const logGroup = title => core.startGroup(title);
34+
const logGroupEnd = () => core.endGroup();
3335

3436
/**
3537
* Initialize Parse Server for benchmarking
@@ -199,9 +201,10 @@ async function measureOperation({ name, operation, iterations, skipWarmup = fals
199201

200202
/**
201203
* Measure GC pressure for an async operation over multiple iterations.
202-
* Tracks garbage collection duration per operation using PerformanceObserver.
203-
* Larger transient allocations (e.g., from unbounded cursor batch sizes) cause
204-
* more frequent and longer GC pauses, which this metric directly captures.
204+
* Tracks total garbage collection time per operation using PerformanceObserver.
205+
* Using total GC time (sum of all pauses) rather than max single pause provides
206+
* much more stable metrics — it eliminates the variance from V8 choosing to do
207+
* one long pause vs. many short pauses for the same amount of GC work.
205208
* @param {Object} options Measurement options.
206209
* @param {string} options.name Name of the operation being measured.
207210
* @param {Function} options.operation Async function to measure.
@@ -236,31 +239,34 @@ async function measureMemoryOperation({ name, operation, iterations, skipWarmup
236239
global.gc();
237240
}
238241

239-
// Track GC events during this iteration; measure the longest single GC pause,
240-
// which reflects the production impact of large transient allocations
241-
let maxGcPause = 0;
242+
// Track GC events during this iteration; sum all GC pause durations to
243+
// measure total GC work, which is stable regardless of whether V8 chooses
244+
// one long pause or many short pauses
245+
let totalGcTime = 0;
242246
const obs = new PerformanceObserver((list) => {
243247
for (const entry of list.getEntries()) {
244-
if (entry.duration > maxGcPause) {
245-
maxGcPause = entry.duration;
246-
}
248+
totalGcTime += entry.duration;
247249
}
248250
});
249251
obs.observe({ type: 'gc', buffered: false });
250252

251253
await operation();
252254

255+
// Force GC after the operation to flush pending GC work into this
256+
// iteration's measurement, preventing cross-iteration contamination
257+
if (typeof global.gc === 'function') {
258+
global.gc();
259+
}
260+
253261
// Flush any buffered entries before disconnecting to avoid data loss
254262
for (const entry of obs.takeRecords()) {
255-
if (entry.duration > maxGcPause) {
256-
maxGcPause = entry.duration;
257-
}
263+
totalGcTime += entry.duration;
258264
}
259265
obs.disconnect();
260-
gcDurations.push(maxGcPause);
266+
gcDurations.push(totalGcTime);
261267

262268
if (LOG_ITERATIONS) {
263-
logInfo(`Iteration ${i + 1}: ${maxGcPause.toFixed(2)} ms GC`);
269+
logInfo(`Iteration ${i + 1}: ${totalGcTime.toFixed(2)} ms GC`);
264270
} else if ((i + 1) % progressInterval === 0 || i + 1 === iterations) {
265271
const progress = Math.round(((i + 1) / iterations) * 100);
266272
logInfo(`Progress: ${progress}%`);
@@ -862,22 +868,38 @@ async function runBenchmarks() {
862868
];
863869

864870
// Run each benchmark with database cleanup
865-
for (const benchmark of benchmarks) {
866-
logInfo(`\nRunning benchmark '${benchmark.name}'...`);
867-
resetParseServer();
868-
await cleanupDatabase();
869-
results.push(await benchmark.fn(benchmark.name));
871+
const suiteStart = performance.now();
872+
for (let idx = 0; idx < benchmarks.length; idx++) {
873+
const benchmark = benchmarks[idx];
874+
const label = `[${idx + 1}/${benchmarks.length}] ${benchmark.name}`;
875+
logGroup(label);
876+
try {
877+
logInfo('Resetting database...');
878+
resetParseServer();
879+
await cleanupDatabase();
880+
logInfo('Running benchmark...');
881+
const benchStart = performance.now();
882+
const result = await benchmark.fn(benchmark.name);
883+
const benchDuration = ((performance.now() - benchStart) / 1000).toFixed(1);
884+
results.push(result);
885+
logInfo(`Result: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`);
886+
logInfo(`Duration: ${benchDuration}s`);
887+
} finally {
888+
logGroupEnd();
889+
}
870890
}
891+
const suiteDuration = ((performance.now() - suiteStart) / 1000).toFixed(1);
871892

872893
// Output results in github-action-benchmark format (stdout)
873894
logInfo(JSON.stringify(results, null, 2));
874895

875-
// Output summary to stderr for visibility
876-
logInfo('Benchmarks completed successfully!');
877-
logInfo('Summary:');
896+
// Output summary
897+
logGroup('Summary');
878898
results.forEach(result => {
879-
logInfo(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`);
899+
logInfo(`${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`);
880900
});
901+
logInfo(`Total duration: ${suiteDuration}s`);
902+
logGroupEnd();
881903

882904
} catch (error) {
883905
logError('Error running benchmarks:', error);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@
135135
"postinstall": "node -p 'require(\"./postinstall.js\")()'",
136136
"madge:circular": "node_modules/.bin/madge ./src --circular",
137137
"benchmark": "cross-env MONGODB_VERSION=8.0.4 MONGODB_TOPOLOGY=standalone mongodb-runner exec -t standalone --version 8.0.4 -- --port 27017 -- npm run benchmark:only",
138-
"benchmark:only": "node --expose-gc benchmark/performance.js",
138+
"benchmark:only": "node --expose-gc --max-old-space-size=1024 benchmark/performance.js",
139139
"benchmark:quick": "cross-env BENCHMARK_ITERATIONS=10 npm run benchmark:only"
140140
},
141141
"types": "types/index.d.ts",

0 commit comments

Comments
 (0)