@@ -267,26 +267,40 @@ const results = [];
267267let db = null ;
268268
269269/**
270- * Check if performance has stabilized (for internal looping).
271- * Matches Python's pytest_plugin.should_stop() logic.
270+ * Check if performance has stabilized, allowing early stopping of benchmarks.
271+ * Matches Python's pytest_plugin.should_stop() logic for consistency.
272+ *
273+ * Performance is considered stable when BOTH conditions are met:
274+ * 1. CENTER: All recent measurements are within ±10% of the median
275+ * 2. SPREAD: The range (max-min) is within 10% of the minimum
276+ *
277+ * @param {Array<number> } runtimes - Array of runtime measurements in microseconds
278+ * @param {number } window - Number of recent measurements to check
279+ * @param {number } minWindowSize - Minimum samples required before checking
280+ * @returns {boolean } True if performance has stabilized
272281 */
273282function shouldStopStability ( runtimes , window , minWindowSize ) {
274283 if ( runtimes . length < window || runtimes . length < minWindowSize ) {
275284 return false ;
276285 }
286+
277287 const recent = runtimes . slice ( - window ) ;
278288 const recentSorted = [ ...recent ] . sort ( ( a , b ) => a - b ) ;
279289 const mid = Math . floor ( window / 2 ) ;
280290 const median = window % 2 ? recentSorted [ mid ] : ( recentSorted [ mid - 1 ] + recentSorted [ mid ] ) / 2 ;
281291
292+ // Check CENTER: all recent points must be close to median
282293 for ( const r of recent ) {
283294 if ( Math . abs ( r - median ) / median > STABILITY_CENTER_TOLERANCE ) {
284295 return false ;
285296 }
286297 }
298+
299+ // Check SPREAD: range must be small relative to minimum
287300 const rMin = recentSorted [ 0 ] ;
288301 const rMax = recentSorted [ recentSorted . length - 1 ] ;
289302 if ( rMin === 0 ) return false ;
303+
290304 return ( rMax - rMin ) / rMin <= STABILITY_SPREAD_TOLERANCE ;
291305}
292306
@@ -775,11 +789,40 @@ function capturePerf(funcName, lineId, fn, ...args) {
775789 return lastReturnValue ;
776790}
777791
792+ /**
793+ * Helper to record async timing and update state.
794+ * @private
795+ */
796+ function _recordAsyncTiming ( startTime , testStdoutTag , durationNs , runtimes ) {
797+ console . log ( `!######${ testStdoutTag } :${ durationNs } ######!` ) ;
798+ sharedPerfState . totalLoopsCompleted ++ ;
799+ if ( durationNs > 0 ) {
800+ runtimes . push ( durationNs / 1000 ) ;
801+ }
802+ }
803+
778804/**
779805 * Async helper for capturePerf to handle async function looping.
780806 * This function awaits promises and continues the benchmark loop properly.
781807 *
782808 * @private
809+ * @param {string } funcName - Name of the function being benchmarked
810+ * @param {string } lineId - Line identifier for this capture point
811+ * @param {Function } fn - The async function to benchmark
812+ * @param {Array } args - Arguments to pass to fn
813+ * @param {Promise } firstPromise - The first promise that was already started
814+ * @param {number } firstStartTime - Start time of the first execution
815+ * @param {string } firstTestStdoutTag - Timing marker tag for the first execution
816+ * @param {string } safeModulePath - Sanitized module path
817+ * @param {string|null } testClassName - Test class name (if any)
818+ * @param {string } safeTestFunctionName - Sanitized test function name
819+ * @param {string } invocationKey - Unique key for this invocation
820+ * @param {Array<number> } runtimes - Array to collect runtimes for stability checking
821+ * @param {number } batchSize - Number of iterations per batch
822+ * @param {number } startBatchIndex - Index where async looping started
823+ * @param {boolean } shouldLoop - Whether to continue looping
824+ * @param {Function } getStabilityWindow - Function to get stability window size
825+ * @returns {Promise } The last return value from fn
783826 */
784827async function _capturePerfAsync (
785828 funcName , lineId , fn , args ,
@@ -796,61 +839,52 @@ async function _capturePerfAsync(
796839 lastReturnValue = await firstPromise ;
797840 const asyncEndTime = getTimeNs ( ) ;
798841 const asyncDurationNs = getDurationNs ( firstStartTime , asyncEndTime ) ;
799- console . log ( `!######${ firstTestStdoutTag } :${ asyncDurationNs } ######!` ) ;
800- sharedPerfState . totalLoopsCompleted ++ ;
801- if ( asyncDurationNs > 0 ) {
802- runtimes . push ( asyncDurationNs / 1000 ) ;
803- }
842+ _recordAsyncTiming ( firstStartTime , firstTestStdoutTag , asyncDurationNs , runtimes ) ;
804843 } catch ( err ) {
805844 const asyncEndTime = getTimeNs ( ) ;
806845 const asyncDurationNs = getDurationNs ( firstStartTime , asyncEndTime ) ;
807- console . log ( `!######${ firstTestStdoutTag } :${ asyncDurationNs } ######!` ) ;
808- sharedPerfState . totalLoopsCompleted ++ ;
809- throw err ;
846+ _recordAsyncTiming ( firstStartTime , firstTestStdoutTag , asyncDurationNs , runtimes ) ;
847+ lastError = err ;
848+ // Don't throw yet - we want to record the timing first
849+ }
850+
851+ // If first iteration failed, stop and throw
852+ if ( lastError ) {
853+ throw lastError ;
810854 }
811855
812856 // Continue looping for remaining iterations
813857 for ( let batchIndex = startBatchIndex + 1 ; batchIndex < batchSize ; batchIndex ++ ) {
814- // Check shared time limit
858+ // Check exit conditions before starting next iteration
815859 if ( shouldLoop && checkSharedTimeLimit ( ) ) {
816860 break ;
817861 }
818862
819- // Check if this invocation has already reached stability
820863 if ( getPerfStabilityCheck ( ) && sharedPerfState . stableInvocations [ invocationKey ] ) {
821864 break ;
822865 }
823866
824- // Get the global loop index for this invocation
825867 const loopIndex = getInvocationLoopIndex ( invocationKey ) ;
826-
827- // Check if we've exceeded max loops
828868 if ( loopIndex > getPerfLoopCount ( ) ) {
829869 break ;
830870 }
831871
832- // Get invocation index for the timing marker
872+ // Generate timing marker identifiers
833873 const testId = `${ safeModulePath } :${ testClassName } :${ safeTestFunctionName } :${ lineId } :${ loopIndex } ` ;
834874 const invocationIndex = getInvocationIndex ( testId ) ;
835875 const invocationId = `${ lineId } _${ invocationIndex } ` ;
836-
837- // Format stdout tag
838876 const testStdoutTag = `${ safeModulePath } :${ testClassName ? testClassName + '.' : '' } ${ safeTestFunctionName } :${ funcName } :${ loopIndex } :${ invocationId } ` ;
839877
878+ // Execute and time the function
840879 try {
841880 const startTime = getTimeNs ( ) ;
842881 lastReturnValue = await fn ( ...args ) ;
843882 const endTime = getTimeNs ( ) ;
844883 const durationNs = getDurationNs ( startTime , endTime ) ;
845884
846- console . log ( `!######${ testStdoutTag } :${ durationNs } ######!` ) ;
847- sharedPerfState . totalLoopsCompleted ++ ;
848-
849- if ( durationNs > 0 ) {
850- runtimes . push ( durationNs / 1000 ) ;
851- }
885+ _recordAsyncTiming ( startTime , testStdoutTag , durationNs , runtimes ) ;
852886
853- // Check stability
887+ // Check if we've reached performance stability
854888 if ( getPerfStabilityCheck ( ) && runtimes . length >= getPerfMinLoops ( ) ) {
855889 const window = getStabilityWindow ( ) ;
856890 if ( shouldStopStability ( runtimes , window , getPerfMinLoops ( ) ) ) {
0 commit comments