@@ -71,6 +71,8 @@ if (!process[PERF_STATE_KEY]) {
7171 shouldStop : false , // Flag to stop all further looping
7272 currentBatch : 0 , // Current batch number (incremented by runner)
7373 invocationLoopCounts : { } , // Track loops per invocation: {invocationKey: loopCount}
74+ invocationRuntimes : { } , // Track runtimes per invocation for stability: {invocationKey: [runtimes]}
75+ stableInvocations : { } , // Invocations that have reached stability: {invocationKey: true}
7476 } ;
7577}
7678const sharedPerfState = process [ PERF_STATE_KEY ] ;
@@ -657,12 +659,26 @@ function capturePerf(funcName, lineId, fn, ...args) {
657659 ? ( hasExternalLoopRunner ? PERF_BATCH_SIZE : PERF_LOOP_COUNT )
658660 : 1 ;
659661
662+ // Initialize runtime tracking for this invocation if needed
663+ if ( ! sharedPerfState . invocationRuntimes [ invocationKey ] ) {
664+ sharedPerfState . invocationRuntimes [ invocationKey ] = [ ] ;
665+ }
666+ const runtimes = sharedPerfState . invocationRuntimes [ invocationKey ] ;
667+
668+ // Calculate stability window size based on collected runtimes
669+ const getStabilityWindow = ( ) => Math . max ( PERF_MIN_LOOPS , Math . ceil ( runtimes . length * STABILITY_WINDOW_SIZE ) ) ;
670+
660671 for ( let batchIndex = 0 ; batchIndex < batchSize ; batchIndex ++ ) {
661672 // Check shared time limit BEFORE each iteration
662673 if ( shouldLoop && checkSharedTimeLimit ( ) ) {
663674 break ;
664675 }
665676
677+ // Check if this invocation has already reached stability
678+ if ( PERF_STABILITY_CHECK && sharedPerfState . stableInvocations [ invocationKey ] ) {
679+ break ;
680+ }
681+
666682 // Get the global loop index for this invocation (increments across batches)
667683 const loopIndex = getInvocationLoopIndex ( invocationKey ) ;
668684
@@ -687,23 +703,17 @@ function capturePerf(funcName, lineId, fn, ...args) {
687703 const endTime = getTimeNs ( ) ;
688704 durationNs = getDurationNs ( startTime , endTime ) ;
689705
690- // Handle promises - for async functions, run once and return
706+ // Handle promises - for async functions, we need to handle looping differently
707+ // Since we can't use await in the sync loop, delegate to async helper
691708 if ( lastReturnValue instanceof Promise ) {
692- return lastReturnValue . then (
693- ( resolved ) => {
694- const asyncEndTime = getTimeNs ( ) ;
695- const asyncDurationNs = getDurationNs ( startTime , asyncEndTime ) ;
696- console . log ( `!######${ testStdoutTag } :${ asyncDurationNs } ######!` ) ;
697- sharedPerfState . totalLoopsCompleted ++ ;
698- return resolved ;
699- } ,
700- ( err ) => {
701- const asyncEndTime = getTimeNs ( ) ;
702- const asyncDurationNs = getDurationNs ( startTime , asyncEndTime ) ;
703- console . log ( `!######${ testStdoutTag } :${ asyncDurationNs } ######!` ) ;
704- sharedPerfState . totalLoopsCompleted ++ ;
705- throw err ;
706- }
709+ // For async functions, delegate to the async looping helper
710+ // Pass along all the context needed for continued looping
711+ return _capturePerfAsync (
712+ funcName , lineId , fn , args ,
713+ lastReturnValue , startTime , testStdoutTag ,
714+ safeModulePath , testClassName , safeTestFunctionName ,
715+ invocationKey , runtimes , batchSize , batchIndex ,
716+ shouldLoop , getStabilityWindow
707717 ) ;
708718 }
709719
@@ -719,6 +729,20 @@ function capturePerf(funcName, lineId, fn, ...args) {
719729 // Update shared loop counter
720730 sharedPerfState . totalLoopsCompleted ++ ;
721731
732+ // Track runtime for stability check (convert to microseconds)
733+ if ( durationNs > 0 ) {
734+ runtimes . push ( durationNs / 1000 ) ;
735+ }
736+
737+ // Check stability after accumulating enough samples
738+ if ( PERF_STABILITY_CHECK && runtimes . length >= PERF_MIN_LOOPS ) {
739+ const window = getStabilityWindow ( ) ;
740+ if ( shouldStopStability ( runtimes , window , PERF_MIN_LOOPS ) ) {
741+ sharedPerfState . stableInvocations [ invocationKey ] = true ;
742+ break ;
743+ }
744+ }
745+
722746 // If we had an error, stop looping
723747 if ( lastError ) {
724748 break ;
@@ -735,6 +759,99 @@ function capturePerf(funcName, lineId, fn, ...args) {
735759 return lastReturnValue ;
736760}
737761
762+ /**
763+ * Async helper for capturePerf to handle async function looping.
764+ * This function awaits promises and continues the benchmark loop properly.
765+ *
766+ * @private
767+ */
768+ async function _capturePerfAsync (
769+ funcName , lineId , fn , args ,
770+ firstPromise , firstStartTime , firstTestStdoutTag ,
771+ safeModulePath , testClassName , safeTestFunctionName ,
772+ invocationKey , runtimes , batchSize , startBatchIndex ,
773+ shouldLoop , getStabilityWindow
774+ ) {
775+ let lastReturnValue ;
776+ let lastError = null ;
777+
778+ // Handle the first promise that was already started
779+ try {
780+ lastReturnValue = await firstPromise ;
781+ const asyncEndTime = getTimeNs ( ) ;
782+ const asyncDurationNs = getDurationNs ( firstStartTime , asyncEndTime ) ;
783+ console . log ( `!######${ firstTestStdoutTag } :${ asyncDurationNs } ######!` ) ;
784+ sharedPerfState . totalLoopsCompleted ++ ;
785+ if ( asyncDurationNs > 0 ) {
786+ runtimes . push ( asyncDurationNs / 1000 ) ;
787+ }
788+ } catch ( err ) {
789+ const asyncEndTime = getTimeNs ( ) ;
790+ const asyncDurationNs = getDurationNs ( firstStartTime , asyncEndTime ) ;
791+ console . log ( `!######${ firstTestStdoutTag } :${ asyncDurationNs } ######!` ) ;
792+ sharedPerfState . totalLoopsCompleted ++ ;
793+ throw err ;
794+ }
795+
796+ // Continue looping for remaining iterations
797+ for ( let batchIndex = startBatchIndex + 1 ; batchIndex < batchSize ; batchIndex ++ ) {
798+ // Check shared time limit
799+ if ( shouldLoop && checkSharedTimeLimit ( ) ) {
800+ break ;
801+ }
802+
803+ // Check if this invocation has already reached stability
804+ if ( PERF_STABILITY_CHECK && sharedPerfState . stableInvocations [ invocationKey ] ) {
805+ break ;
806+ }
807+
808+ // Get the global loop index for this invocation
809+ const loopIndex = getInvocationLoopIndex ( invocationKey ) ;
810+
811+ // Check if we've exceeded max loops
812+ if ( loopIndex > PERF_LOOP_COUNT ) {
813+ break ;
814+ }
815+
816+ // Get invocation index for the timing marker
817+ const testId = `${ safeModulePath } :${ testClassName } :${ safeTestFunctionName } :${ lineId } :${ loopIndex } ` ;
818+ const invocationIndex = getInvocationIndex ( testId ) ;
819+ const invocationId = `${ lineId } _${ invocationIndex } ` ;
820+
821+ // Format stdout tag
822+ const testStdoutTag = `${ safeModulePath } :${ testClassName ? testClassName + '.' : '' } ${ safeTestFunctionName } :${ funcName } :${ loopIndex } :${ invocationId } ` ;
823+
824+ try {
825+ const startTime = getTimeNs ( ) ;
826+ lastReturnValue = await fn ( ...args ) ;
827+ const endTime = getTimeNs ( ) ;
828+ const durationNs = getDurationNs ( startTime , endTime ) ;
829+
830+ console . log ( `!######${ testStdoutTag } :${ durationNs } ######!` ) ;
831+ sharedPerfState . totalLoopsCompleted ++ ;
832+
833+ if ( durationNs > 0 ) {
834+ runtimes . push ( durationNs / 1000 ) ;
835+ }
836+
837+ // Check stability
838+ if ( PERF_STABILITY_CHECK && runtimes . length >= PERF_MIN_LOOPS ) {
839+ const window = getStabilityWindow ( ) ;
840+ if ( shouldStopStability ( runtimes , window , PERF_MIN_LOOPS ) ) {
841+ sharedPerfState . stableInvocations [ invocationKey ] = true ;
842+ break ;
843+ }
844+ }
845+ } catch ( e ) {
846+ lastError = e ;
847+ break ;
848+ }
849+ }
850+
851+ if ( lastError ) throw lastError ;
852+ return lastReturnValue ;
853+ }
854+
738855/**
739856 * Capture multiple invocations for benchmarking.
740857 *
@@ -790,6 +907,8 @@ function resetPerfState() {
790907 sharedPerfState . startTime = null ;
791908 sharedPerfState . totalLoopsCompleted = 0 ;
792909 sharedPerfState . shouldStop = false ;
910+ sharedPerfState . invocationRuntimes = { } ;
911+ sharedPerfState . stableInvocations = { } ;
793912}
794913
795914/**
0 commit comments