@@ -30,6 +30,8 @@ let core;
3030// Logging helpers
3131const logInfo = message => core . info ( message ) ;
3232const 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 ) ;
0 commit comments