Skip to content

Commit 04a87cf

Browse files
fix: add Jest 30 support, fix time limit, and fix async function looping
- Add Jest 30 compatibility by detecting version and using TestRunner class - Resolve jest-runner from project's node_modules instead of codeflash's bundle - Fix time limit enforcement by using local time tracking instead of shared state (Jest runs tests in worker processes, so state isn't shared with runner) - Integrate stability-based early stopping into capturePerf - Use plain object instead of Set for stableInvocations to survive Jest module resets - Fix async function benchmarking: properly loop through iterations using async helper (Previously, async functions only got one timing marker due to early return) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7bc7032 commit 04a87cf

2 files changed

Lines changed: 264 additions & 43 deletions

File tree

packages/codeflash/runtime/capture.js

Lines changed: 135 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
7678
const 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

Comments
 (0)