codeflash 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "codeflash",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Codeflash - AI-powered code optimization for JavaScript and TypeScript",
5
5
  "main": "runtime/index.js",
6
+ "types": "runtime/index.d.ts",
6
7
  "bin": {
7
8
  "codeflash": "./bin/codeflash.js",
8
9
  "codeflash-setup": "./bin/codeflash-setup.js"
@@ -12,6 +13,7 @@
12
13
  },
13
14
  "exports": {
14
15
  ".": {
16
+ "types": "./runtime/index.d.ts",
15
17
  "require": "./runtime/index.js",
16
18
  "import": "./runtime/index.js"
17
19
  },
@@ -26,6 +28,10 @@
26
28
  "./comparator": {
27
29
  "require": "./runtime/comparator.js",
28
30
  "import": "./runtime/comparator.js"
31
+ },
32
+ "./loop-runner": {
33
+ "require": "./runtime/loop-runner.js",
34
+ "import": "./runtime/loop-runner.js"
29
35
  }
30
36
  },
31
37
  "scripts": {
@@ -63,14 +69,21 @@
63
69
  "node": ">=18.0.0"
64
70
  },
65
71
  "peerDependencies": {
66
- "jest": ">=27.0.0"
72
+ "jest": ">=27.0.0",
73
+ "jest-runner": ">=27.0.0"
67
74
  },
68
75
  "peerDependenciesMeta": {
69
76
  "jest": {
70
77
  "optional": true
78
+ },
79
+ "jest-runner": {
80
+ "optional": true
71
81
  }
72
82
  },
73
- "optionalDependencies": {
74
- "better-sqlite3": "^9.0.0"
83
+ "dependencies": {
84
+ "better-sqlite3": "^12.0.0",
85
+ "@msgpack/msgpack": "^3.0.0",
86
+ "jest-runner": "^29.7.0",
87
+ "jest-junit": "^16.0.0"
75
88
  }
76
89
  }
@@ -28,30 +28,111 @@
28
28
 
29
29
  const fs = require('fs');
30
30
  const path = require('path');
31
+ const Database = require('better-sqlite3');
31
32
 
32
33
  // Load the codeflash serializer for robust value serialization
33
34
  const serializer = require('./serializer');
34
35
 
35
36
  // Try to load better-sqlite3, fall back to JSON if not available
36
- let Database;
37
37
  let useSqlite = false;
38
- try {
39
- Database = require('better-sqlite3');
40
- useSqlite = true;
41
- } catch (e) {
42
- // better-sqlite3 not available, will use JSON fallback
43
- console.warn('[codeflash] better-sqlite3 not found, using JSON fallback');
44
- }
45
38
 
46
39
  // Configuration from environment
47
- const OUTPUT_FILE = process.env.CODEFLASH_OUTPUT_FILE || '/tmp/codeflash_results.sqlite';
40
+ const OUTPUT_FILE = process.env.CODEFLASH_OUTPUT_FILE;
48
41
  const LOOP_INDEX = parseInt(process.env.CODEFLASH_LOOP_INDEX || '1', 10);
49
- const TEST_ITERATION = process.env.CODEFLASH_TEST_ITERATION || '0';
50
- const TEST_MODULE = process.env.CODEFLASH_TEST_MODULE || '';
42
+ const TEST_ITERATION = process.env.CODEFLASH_TEST_ITERATION;
43
+ const TEST_MODULE = process.env.CODEFLASH_TEST_MODULE;
44
+
45
+ // Performance loop configuration - controls batched looping in capturePerf
46
+ // Batched looping ensures fair distribution across all test invocations:
47
+ // Batch 1: Test1(5 loops) → Test2(5 loops) → Test3(5 loops)
48
+ // Batch 2: Test1(5 loops) → Test2(5 loops) → Test3(5 loops)
49
+ // ...until time budget exhausted
50
+ const PERF_LOOP_COUNT = parseInt(process.env.CODEFLASH_PERF_LOOP_COUNT || '1', 10);
51
+ const PERF_MIN_LOOPS = parseInt(process.env.CODEFLASH_PERF_MIN_LOOPS || '5', 10);
52
+ const PERF_TARGET_DURATION_MS = parseInt(process.env.CODEFLASH_PERF_TARGET_DURATION_MS || '10000', 10);
53
+ const PERF_BATCH_SIZE = parseInt(process.env.CODEFLASH_PERF_BATCH_SIZE || '10', 10);
54
+ const PERF_STABILITY_CHECK = (process.env.CODEFLASH_PERF_STABILITY_CHECK || 'false').toLowerCase() === 'true';
55
+ // Current batch number - set by loop-runner before each batch
56
+ // This allows continuous loop indices even when Jest resets module state
57
+ const PERF_CURRENT_BATCH = parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '0', 10);
58
+
59
+ // Stability constants (matching Python's config_consts.py)
60
+ const STABILITY_WINDOW_SIZE = 0.35;
61
+ const STABILITY_CENTER_TOLERANCE = 0.0025;
62
+ const STABILITY_SPREAD_TOLERANCE = 0.0025;
63
+
64
+ // Shared state for coordinating batched looping across all capturePerf calls
65
+ // Uses process object to persist across Jest's module reloads per test file
66
+ const PERF_STATE_KEY = '__codeflash_perf_state__';
67
+ if (!process[PERF_STATE_KEY]) {
68
+ process[PERF_STATE_KEY] = {
69
+ startTime: null, // When benchmarking started
70
+ totalLoopsCompleted: 0, // Total loops across all invocations
71
+ shouldStop: false, // Flag to stop all further looping
72
+ currentBatch: 0, // Current batch number (incremented by runner)
73
+ invocationLoopCounts: {}, // Track loops per invocation: {invocationKey: loopCount}
74
+ };
75
+ }
76
+ const sharedPerfState = process[PERF_STATE_KEY];
77
+
78
+ /**
79
+ * Check if the shared time budget has been exceeded.
80
+ * @returns {boolean} True if we should stop looping
81
+ */
82
+ function checkSharedTimeLimit() {
83
+ if (sharedPerfState.shouldStop) return true;
84
+ if (sharedPerfState.startTime === null) {
85
+ sharedPerfState.startTime = Date.now();
86
+ return false;
87
+ }
88
+ const elapsed = Date.now() - sharedPerfState.startTime;
89
+ if (elapsed >= PERF_TARGET_DURATION_MS && sharedPerfState.totalLoopsCompleted >= PERF_MIN_LOOPS) {
90
+ sharedPerfState.shouldStop = true;
91
+ return true;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Get the current loop index for a specific invocation.
98
+ * Each invocation tracks its own loop count independently within a batch.
99
+ * The actual loop index is computed as: (batch - 1) * BATCH_SIZE + localIndex
100
+ * This ensures continuous loop indices even when Jest resets module state.
101
+ * @param {string} invocationKey - Unique key for this test invocation
102
+ * @returns {number} The next global loop index for this invocation
103
+ */
104
+ function getInvocationLoopIndex(invocationKey) {
105
+ // Track local loop count within this batch (starts at 0)
106
+ if (!sharedPerfState.invocationLoopCounts[invocationKey]) {
107
+ sharedPerfState.invocationLoopCounts[invocationKey] = 0;
108
+ }
109
+ const localIndex = ++sharedPerfState.invocationLoopCounts[invocationKey];
110
+
111
+ // Calculate global loop index using batch number from environment
112
+ // PERF_CURRENT_BATCH is 1-based (set by loop-runner before each batch)
113
+ const currentBatch = parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '1', 10);
114
+ const globalIndex = (currentBatch - 1) * PERF_BATCH_SIZE + localIndex;
115
+
116
+ return globalIndex;
117
+ }
118
+
119
+ /**
120
+ * Increment the batch counter. Called by loop-runner between test file runs.
121
+ */
122
+ function incrementBatch() {
123
+ sharedPerfState.currentBatch++;
124
+ }
125
+
126
+ /**
127
+ * Get current batch number.
128
+ */
129
+ function getCurrentBatch() {
130
+ return sharedPerfState.currentBatch;
131
+ }
51
132
 
52
133
  // Random seed for reproducible test runs
53
134
  // Both original and optimized runs use the same seed to get identical "random" values
54
- const RANDOM_SEED = parseInt(process.env.CODEFLASH_RANDOM_SEED || '0', 10);
135
+ const RANDOM_SEED = parseInt(process.env.CODEFLASH_RANDOM_SEED, 10);
55
136
 
56
137
  /**
57
138
  * Seeded random number generator using mulberry32 algorithm.
@@ -167,6 +248,30 @@ const results = [];
167
248
  // SQLite database (lazy initialized)
168
249
  let db = null;
169
250
 
251
+ /**
252
+ * Check if performance has stabilized (for internal looping).
253
+ * Matches Python's pytest_plugin.should_stop() logic.
254
+ */
255
+ function shouldStopStability(runtimes, window, minWindowSize) {
256
+ if (runtimes.length < window || runtimes.length < minWindowSize) {
257
+ return false;
258
+ }
259
+ const recent = runtimes.slice(-window);
260
+ const recentSorted = [...recent].sort((a, b) => a - b);
261
+ const mid = Math.floor(window / 2);
262
+ const median = window % 2 ? recentSorted[mid] : (recentSorted[mid - 1] + recentSorted[mid]) / 2;
263
+
264
+ for (const r of recent) {
265
+ if (Math.abs(r - median) / median > STABILITY_CENTER_TOLERANCE) {
266
+ return false;
267
+ }
268
+ }
269
+ const rMin = recentSorted[0];
270
+ const rMax = recentSorted[recentSorted.length - 1];
271
+ if (rMin === 0) return false;
272
+ return (rMax - rMin) / rMin <= STABILITY_SPREAD_TOLERANCE;
273
+ }
274
+
170
275
  /**
171
276
  * Get high-resolution time in nanoseconds.
172
277
  * Prefers process.hrtime.bigint() for nanosecond precision,
@@ -484,7 +589,9 @@ function capture(funcName, lineId, fn, ...args) {
484
589
  * It prints start/end tags to stdout (no SQLite writes, no serialization overhead).
485
590
  * Used when we've already verified behavior and just need accurate timing.
486
591
  *
487
- * The timing measurement is done exactly around the function call for accuracy.
592
+ * When CODEFLASH_PERF_LOOP_COUNT > 1, this function loops internally to avoid
593
+ * Jest environment overhead per iteration. This dramatically improves utilization
594
+ * (time spent in actual function execution vs overhead).
488
595
  *
489
596
  * Output format matches Python's codeflash_performance wrapper:
490
597
  * Start: !$######test_module:test_class.test_name:func_name:loop_index:invocation_id######$!
@@ -498,16 +605,16 @@ function capture(funcName, lineId, fn, ...args) {
498
605
  * @throws {Error} - Re-throws any error from the function
499
606
  */
500
607
  function capturePerf(funcName, lineId, fn, ...args) {
501
- // Get test context
502
- // Use TEST_MODULE env var if set, otherwise derive from test file path
608
+ // Check if we should skip looping entirely (shared time budget exceeded)
609
+ const shouldLoop = PERF_LOOP_COUNT > 1 && !checkSharedTimeLimit();
610
+
611
+ // Get test context (computed once, reused across batch)
503
612
  let testModulePath;
504
613
  if (TEST_MODULE) {
505
614
  testModulePath = TEST_MODULE;
506
615
  } else if (currentTestPath) {
507
- // Get relative path from cwd and convert to module-style path
508
616
  const path = require('path');
509
617
  const relativePath = path.relative(process.cwd(), currentTestPath);
510
- // Convert to Python module-style path (e.g., "tests/test_foo.test.js" -> "tests.test_foo.test")
511
618
  testModulePath = relativePath
512
619
  .replace(/\\/g, '/')
513
620
  .replace(/\.js$/, '')
@@ -516,70 +623,109 @@ function capturePerf(funcName, lineId, fn, ...args) {
516
623
  } else {
517
624
  testModulePath = currentTestName || 'unknown';
518
625
  }
519
- const testClassName = null; // Jest doesn't use classes like Python
626
+ const testClassName = null;
520
627
  const testFunctionName = currentTestName || 'unknown';
521
628
 
522
- // Sanitized versions for stdout tags (avoid regex conflicts)
523
629
  const safeModulePath = sanitizeTestId(testModulePath);
524
630
  const safeTestFunctionName = sanitizeTestId(testFunctionName);
525
631
 
526
- // Create testId for invocation tracking (matches Python format)
527
- const testId = `${safeModulePath}:${testClassName}:${safeTestFunctionName}:${lineId}:${LOOP_INDEX}`;
632
+ // Create unique key for this invocation (identifies this specific capturePerf call site)
633
+ const invocationKey = `${safeModulePath}:${testClassName}:${safeTestFunctionName}:${funcName}:${lineId}`;
528
634
 
529
- // Get invocation index (increments if same testId seen again)
530
- const invocationIndex = getInvocationIndex(testId);
531
- const invocationId = `${lineId}_${invocationIndex}`;
635
+ // Check if we've already completed all loops for this invocation
636
+ // If so, just execute the function once without timing (for test assertions)
637
+ const peekLoopIndex = (sharedPerfState.invocationLoopCounts[invocationKey] || 0);
638
+ const currentBatch = parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '1', 10);
639
+ const nextGlobalIndex = (currentBatch - 1) * PERF_BATCH_SIZE + peekLoopIndex + 1;
532
640
 
533
- // Format stdout tag (matches Python format, uses sanitized names)
534
- const testStdoutTag = `${safeModulePath}:${testClassName ? testClassName + '.' : ''}${safeTestFunctionName}:${funcName}:${LOOP_INDEX}:${invocationId}`;
641
+ if (shouldLoop && nextGlobalIndex > PERF_LOOP_COUNT) {
642
+ // All loops completed, just execute once for test assertion
643
+ return fn(...args);
644
+ }
535
645
 
536
- // Print start tag
537
- console.log(`!$######${testStdoutTag}######$!`);
646
+ let lastReturnValue;
647
+ let lastError = null;
538
648
 
539
- // Timing with nanosecond precision - exactly around the function call
540
- let returnValue;
541
- let error = null;
542
- let durationNs;
649
+ // Batched looping: run BATCH_SIZE loops per capturePerf call
650
+ // This ensures fair distribution across all test invocations
651
+ const batchSize = shouldLoop ? PERF_BATCH_SIZE : 1;
543
652
 
544
- try {
545
- const startTime = getTimeNs();
546
- returnValue = fn(...args);
547
- const endTime = getTimeNs();
548
- durationNs = getDurationNs(startTime, endTime);
653
+ for (let batchIndex = 0; batchIndex < batchSize; batchIndex++) {
654
+ // Check shared time limit BEFORE each iteration
655
+ if (shouldLoop && checkSharedTimeLimit()) {
656
+ break;
657
+ }
549
658
 
550
- // Handle promises (async functions)
551
- if (returnValue instanceof Promise) {
552
- return returnValue.then(
553
- (resolved) => {
554
- // For async, we measure until resolution
555
- const asyncEndTime = getTimeNs();
556
- const asyncDurationNs = getDurationNs(startTime, asyncEndTime);
557
- // Print end tag with timing
558
- console.log(`!######${testStdoutTag}:${asyncDurationNs}######!`);
559
- return resolved;
560
- },
561
- (err) => {
562
- const asyncEndTime = getTimeNs();
563
- const asyncDurationNs = getDurationNs(startTime, asyncEndTime);
564
- // Print end tag with timing even on error
565
- console.log(`!######${testStdoutTag}:${asyncDurationNs}######!`);
566
- throw err;
567
- }
568
- );
659
+ // Get the global loop index for this invocation (increments across batches)
660
+ const loopIndex = getInvocationLoopIndex(invocationKey);
661
+
662
+ // Check if we've exceeded max loops for this invocation
663
+ if (loopIndex > PERF_LOOP_COUNT) {
664
+ break;
665
+ }
666
+
667
+ // Get invocation index for the timing marker
668
+ const testId = `${safeModulePath}:${testClassName}:${safeTestFunctionName}:${lineId}:${loopIndex}`;
669
+ const invocationIndex = getInvocationIndex(testId);
670
+ const invocationId = `${lineId}_${invocationIndex}`;
671
+
672
+ // Format stdout tag with current loop index
673
+ const testStdoutTag = `${safeModulePath}:${testClassName ? testClassName + '.' : ''}${safeTestFunctionName}:${funcName}:${loopIndex}:${invocationId}`;
674
+
675
+ // Timing with nanosecond precision
676
+ let durationNs;
677
+ try {
678
+ const startTime = getTimeNs();
679
+ lastReturnValue = fn(...args);
680
+ const endTime = getTimeNs();
681
+ durationNs = getDurationNs(startTime, endTime);
682
+
683
+ // Handle promises - for async functions, run once and return
684
+ if (lastReturnValue instanceof Promise) {
685
+ return lastReturnValue.then(
686
+ (resolved) => {
687
+ const asyncEndTime = getTimeNs();
688
+ const asyncDurationNs = getDurationNs(startTime, asyncEndTime);
689
+ console.log(`!######${testStdoutTag}:${asyncDurationNs}######!`);
690
+ sharedPerfState.totalLoopsCompleted++;
691
+ return resolved;
692
+ },
693
+ (err) => {
694
+ const asyncEndTime = getTimeNs();
695
+ const asyncDurationNs = getDurationNs(startTime, asyncEndTime);
696
+ console.log(`!######${testStdoutTag}:${asyncDurationNs}######!`);
697
+ sharedPerfState.totalLoopsCompleted++;
698
+ throw err;
699
+ }
700
+ );
701
+ }
702
+
703
+ lastError = null;
704
+ } catch (e) {
705
+ durationNs = 0;
706
+ lastError = e;
707
+ }
708
+
709
+ // Print end tag with timing
710
+ console.log(`!######${testStdoutTag}:${durationNs}######!`);
711
+
712
+ // Update shared loop counter
713
+ sharedPerfState.totalLoopsCompleted++;
714
+
715
+ // If we had an error, stop looping
716
+ if (lastError) {
717
+ break;
569
718
  }
570
- } catch (e) {
571
- const endTime = getTimeNs();
572
- // For sync errors, we still need to calculate duration
573
- // Use a fallback if we didn't capture startTime yet
574
- durationNs = 0;
575
- error = e;
576
719
  }
577
720
 
578
- // Print end tag with timing (no rounding)
579
- console.log(`!######${testStdoutTag}:${durationNs}######!`);
721
+ if (lastError) throw lastError;
580
722
 
581
- if (error) throw error;
582
- return returnValue;
723
+ // If we never executed (e.g., hit loop limit on first iteration), run once for assertion
724
+ if (lastReturnValue === undefined && !lastError) {
725
+ return fn(...args);
726
+ }
727
+
728
+ return lastReturnValue;
583
729
  }
584
730
 
585
731
  /**
@@ -629,6 +775,16 @@ function writeResults() {
629
775
  }
630
776
  }
631
777
 
778
+ /**
779
+ * Reset shared performance state.
780
+ * Should be called at the start of each test file to reset timing.
781
+ */
782
+ function resetPerfState() {
783
+ sharedPerfState.startTime = null;
784
+ sharedPerfState.totalLoopsCompleted = 0;
785
+ sharedPerfState.shouldStop = false;
786
+ }
787
+
632
788
  /**
633
789
  * Clear all recorded results.
634
790
  * Useful for resetting between test files.
@@ -636,6 +792,7 @@ function writeResults() {
636
792
  function clearResults() {
637
793
  results.length = 0;
638
794
  resetInvocationCounters();
795
+ resetPerfState();
639
796
  }
640
797
 
641
798
  /**
@@ -698,10 +855,17 @@ module.exports = {
698
855
  resetInvocationCounters,
699
856
  getInvocationIndex,
700
857
  sanitizeTestId, // Sanitize test names for stdout tags
858
+ // Batch looping control (used by loop-runner)
859
+ incrementBatch,
860
+ getCurrentBatch,
861
+ checkSharedTimeLimit,
701
862
  // Serializer info
702
863
  getSerializerType: serializer.getSerializerType,
703
864
  // Constants
704
865
  LOOP_INDEX,
705
866
  OUTPUT_FILE,
706
- TEST_ITERATION
867
+ TEST_ITERATION,
868
+ // Batch configuration
869
+ PERF_BATCH_SIZE,
870
+ PERF_LOOP_COUNT,
707
871
  };
@@ -32,7 +32,8 @@ const path = require('path');
32
32
  const { deserialize } = require('./serializer');
33
33
  const { comparator } = require('./comparator');
34
34
 
35
- // Lazy-load better-sqlite3 to avoid exit on require()
35
+ // Lazy-load better-sqlite3 to avoid process.exit during module require
36
+ // This prevents crashes when this module is imported by test files that don't use it
36
37
  let Database = null;
37
38
  let databaseLoadError = null;
38
39
 
@@ -60,6 +61,7 @@ function readTestResults(dbPath) {
60
61
  throw new Error(`Database not found: ${dbPath}`);
61
62
  }
62
63
 
64
+ // Get Database lazily - throws if not available
63
65
  const { Database: DB, error } = getDatabase();
64
66
  if (error) {
65
67
  throw new Error(error);
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Codeflash TypeScript Declarations
3
+ */
4
+
5
+ /**
6
+ * Capture a function call for behavior verification.
7
+ * Records inputs, outputs, timing to SQLite database.
8
+ *
9
+ * @param funcName - Name of the function being tested
10
+ * @param lineId - Line number identifier in test file
11
+ * @param fn - The function to call
12
+ * @param args - Arguments to pass to the function
13
+ * @returns The function's return value
14
+ */
15
+ export function capture<T extends (...args: any[]) => any>(
16
+ funcName: string,
17
+ lineId: string,
18
+ fn: T,
19
+ ...args: Parameters<T>
20
+ ): ReturnType<T>;
21
+
22
+ /**
23
+ * Capture a function call for performance benchmarking.
24
+ * Only measures timing, prints to stdout.
25
+ *
26
+ * @param funcName - Name of the function being tested
27
+ * @param lineId - Line number identifier in test file
28
+ * @param fn - The function to call
29
+ * @param args - Arguments to pass to the function
30
+ * @returns The function's return value
31
+ */
32
+ export function capturePerf<T extends (...args: any[]) => any>(
33
+ funcName: string,
34
+ lineId: string,
35
+ fn: T,
36
+ ...args: Parameters<T>
37
+ ): ReturnType<T>;
38
+
39
+ /**
40
+ * Capture multiple invocations for benchmarking.
41
+ *
42
+ * @param funcName - Name of the function being tested
43
+ * @param lineId - Line number identifier
44
+ * @param fn - The function to call
45
+ * @param argsList - List of argument arrays to test
46
+ * @returns Array of return values
47
+ */
48
+ export function captureMultiple<T extends (...args: any[]) => any>(
49
+ funcName: string,
50
+ lineId: string,
51
+ fn: T,
52
+ argsList: Parameters<T>[]
53
+ ): ReturnType<T>[];
54
+
55
+ /**
56
+ * Write remaining results to file.
57
+ */
58
+ export function writeResults(): void;
59
+
60
+ /**
61
+ * Clear all recorded results.
62
+ */
63
+ export function clearResults(): void;
64
+
65
+ /**
66
+ * Get the current results buffer.
67
+ */
68
+ export function getResults(): any[];
69
+
70
+ /**
71
+ * Set the current test name.
72
+ */
73
+ export function setTestName(name: string): void;
74
+
75
+ /**
76
+ * Serialize a value for storage.
77
+ */
78
+ export function safeSerialize(value: any): Buffer;
79
+
80
+ /**
81
+ * Deserialize a buffer back to a value.
82
+ */
83
+ export function safeDeserialize(buffer: Buffer | Uint8Array): any;
84
+
85
+ /**
86
+ * Initialize the SQLite database.
87
+ */
88
+ export function initDatabase(): void;
89
+
90
+ /**
91
+ * Reset invocation counters.
92
+ */
93
+ export function resetInvocationCounters(): void;
94
+
95
+ /**
96
+ * Get invocation index for a testId.
97
+ */
98
+ export function getInvocationIndex(testId: string): number;
99
+
100
+ /**
101
+ * Sanitize a string for use in test IDs.
102
+ */
103
+ export function sanitizeTestId(str: string): string;
104
+
105
+ /**
106
+ * Get the serializer type being used.
107
+ */
108
+ export function getSerializerType(): 'v8' | 'msgpack';
109
+
110
+ /**
111
+ * Current loop index from environment.
112
+ */
113
+ export const LOOP_INDEX: number;
114
+
115
+ /**
116
+ * Output file path from environment.
117
+ */
118
+ export const OUTPUT_FILE: string;
119
+
120
+ /**
121
+ * Test iteration from environment.
122
+ */
123
+ export const TEST_ITERATION: string;
124
+
125
+ // Default export for CommonJS compatibility
126
+ declare const codeflash: {
127
+ capture: typeof capture;
128
+ capturePerf: typeof capturePerf;
129
+ captureMultiple: typeof captureMultiple;
130
+ writeResults: typeof writeResults;
131
+ clearResults: typeof clearResults;
132
+ getResults: typeof getResults;
133
+ setTestName: typeof setTestName;
134
+ safeSerialize: typeof safeSerialize;
135
+ safeDeserialize: typeof safeDeserialize;
136
+ initDatabase: typeof initDatabase;
137
+ resetInvocationCounters: typeof resetInvocationCounters;
138
+ getInvocationIndex: typeof getInvocationIndex;
139
+ sanitizeTestId: typeof sanitizeTestId;
140
+ getSerializerType: typeof getSerializerType;
141
+ LOOP_INDEX: typeof LOOP_INDEX;
142
+ OUTPUT_FILE: typeof OUTPUT_FILE;
143
+ TEST_ITERATION: typeof TEST_ITERATION;
144
+ };
145
+
146
+ export default codeflash;
package/runtime/index.js CHANGED
@@ -73,6 +73,13 @@ module.exports = {
73
73
  OUTPUT_FILE: capture.OUTPUT_FILE,
74
74
  TEST_ITERATION: capture.TEST_ITERATION,
75
75
 
76
+ // === Batch Looping Control (used by loop-runner) ===
77
+ incrementBatch: capture.incrementBatch,
78
+ getCurrentBatch: capture.getCurrentBatch,
79
+ checkSharedTimeLimit: capture.checkSharedTimeLimit,
80
+ PERF_BATCH_SIZE: capture.PERF_BATCH_SIZE,
81
+ PERF_LOOP_COUNT: capture.PERF_LOOP_COUNT,
82
+
76
83
  // === Feature Detection ===
77
84
  hasV8: serializer.hasV8,
78
85
  hasMsgpack: serializer.hasMsgpack,
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Codeflash Loop Runner - Custom Jest Test Runner for Performance Benchmarking
3
+ *
4
+ * Implements BATCHED LOOPING for fair distribution across all test invocations:
5
+ *
6
+ * Batch 1: Test1(5 loops) → Test2(5 loops) → Test3(5 loops) → ...
7
+ * Batch 2: Test1(5 loops) → Test2(5 loops) → Test3(5 loops) → ...
8
+ * ...until time budget exhausted
9
+ *
10
+ * This ensures:
11
+ * - Fair distribution: All test invocations get equal loop counts
12
+ * - Batched overhead: Console.log overhead amortized over batches
13
+ * - Good utilization: Time budget shared across all tests
14
+ *
15
+ * Configuration via environment variables:
16
+ * CODEFLASH_PERF_LOOP_COUNT - Max loops per invocation (default: 10000)
17
+ * CODEFLASH_PERF_BATCH_SIZE - Loops per batch (default: 5)
18
+ * CODEFLASH_PERF_MIN_LOOPS - Min loops before stopping (default: 5)
19
+ * CODEFLASH_PERF_TARGET_DURATION_MS - Target total duration (default: 10000)
20
+ *
21
+ * Usage:
22
+ * npx jest --runner=codeflash/loop-runner
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ const { createRequire } = require('module');
28
+ const path = require('path');
29
+
30
+ const jestRunnerPath = require.resolve('jest-runner');
31
+ const internalRequire = createRequire(jestRunnerPath);
32
+ const runTest = internalRequire('./runTest').default;
33
+
34
+ // Configuration
35
+ const MAX_BATCHES = parseInt(process.env.CODEFLASH_PERF_LOOP_COUNT || '10000', 10);
36
+ const TARGET_DURATION_MS = parseInt(process.env.CODEFLASH_PERF_TARGET_DURATION_MS || '10000', 10);
37
+ const MIN_BATCHES = parseInt(process.env.CODEFLASH_PERF_MIN_LOOPS || '5', 10);
38
+
39
+ /**
40
+ * Simple event emitter for Jest compatibility.
41
+ */
42
+ class SimpleEventEmitter {
43
+ constructor() {
44
+ this.listeners = new Map();
45
+ }
46
+
47
+ on(eventName, listener) {
48
+ if (!this.listeners.has(eventName)) {
49
+ this.listeners.set(eventName, new Set());
50
+ }
51
+ this.listeners.get(eventName).add(listener);
52
+ return () => {
53
+ const set = this.listeners.get(eventName);
54
+ if (set) set.delete(listener);
55
+ };
56
+ }
57
+
58
+ async emit(eventName, data) {
59
+ const set = this.listeners.get(eventName);
60
+ if (set) {
61
+ for (const listener of set) {
62
+ await listener(data);
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Deep copy utility.
70
+ */
71
+ function deepCopy(obj, seen = new WeakMap()) {
72
+ if (obj === null || typeof obj !== 'object') return obj;
73
+ if (seen.has(obj)) return seen.get(obj);
74
+ if (Array.isArray(obj)) {
75
+ const copy = [];
76
+ seen.set(obj, copy);
77
+ for (let i = 0; i < obj.length; i++) copy[i] = deepCopy(obj[i], seen);
78
+ return copy;
79
+ }
80
+ if (obj instanceof Date) return new Date(obj.getTime());
81
+ if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
82
+ const copy = {};
83
+ seen.set(obj, copy);
84
+ for (const key of Object.keys(obj)) copy[key] = deepCopy(obj[key], seen);
85
+ return copy;
86
+ }
87
+
88
+ /**
89
+ * Codeflash Loop Runner with Batched Looping
90
+ */
91
+ class CodeflashLoopRunner {
92
+ constructor(globalConfig, context) {
93
+ this._globalConfig = globalConfig;
94
+ this._context = context || {};
95
+ this._eventEmitter = new SimpleEventEmitter();
96
+ }
97
+
98
+ get supportsEventEmitters() {
99
+ return true;
100
+ }
101
+
102
+ get isSerial() {
103
+ return true;
104
+ }
105
+
106
+ on(eventName, listener) {
107
+ return this._eventEmitter.on(eventName, listener);
108
+ }
109
+
110
+ /**
111
+ * Run tests with batched looping for fair distribution.
112
+ */
113
+ async runTests(tests, watcher, options) {
114
+ const startTime = Date.now();
115
+ let batchCount = 0;
116
+ let hasFailure = false;
117
+ let allConsoleOutput = '';
118
+
119
+ // Import shared state functions from capture module
120
+ // We need to do this dynamically since the module may be reloaded
121
+ let checkSharedTimeLimit;
122
+ let incrementBatch;
123
+ try {
124
+ const capture = require('codeflash');
125
+ checkSharedTimeLimit = capture.checkSharedTimeLimit;
126
+ incrementBatch = capture.incrementBatch;
127
+ } catch (e) {
128
+ // Fallback if codeflash module not available
129
+ checkSharedTimeLimit = () => {
130
+ const elapsed = Date.now() - startTime;
131
+ return elapsed >= TARGET_DURATION_MS && batchCount >= MIN_BATCHES;
132
+ };
133
+ incrementBatch = () => {};
134
+ }
135
+
136
+ // Batched looping: run all test files multiple times
137
+ while (batchCount < MAX_BATCHES) {
138
+ batchCount++;
139
+
140
+ // Check time limit BEFORE each batch
141
+ if (batchCount > MIN_BATCHES && checkSharedTimeLimit()) {
142
+ break;
143
+ }
144
+
145
+ // Check if interrupted
146
+ if (watcher.isInterrupted()) {
147
+ break;
148
+ }
149
+
150
+ // Increment batch counter in shared state and set env var
151
+ // The env var persists across Jest module resets, ensuring continuous loop indices
152
+ incrementBatch();
153
+ process.env.CODEFLASH_PERF_CURRENT_BATCH = String(batchCount);
154
+
155
+ // Run all test files in this batch
156
+ const batchResult = await this._runAllTestsOnce(tests, watcher);
157
+ allConsoleOutput += batchResult.consoleOutput;
158
+
159
+ if (batchResult.hasFailure) {
160
+ hasFailure = true;
161
+ break;
162
+ }
163
+
164
+ // Check time limit AFTER each batch
165
+ if (checkSharedTimeLimit()) {
166
+ break;
167
+ }
168
+ }
169
+
170
+ const totalTimeMs = Date.now() - startTime;
171
+
172
+ // Output all collected console logs - this is critical for timing marker extraction
173
+ // The console output contains the !######...######! timing markers from capturePerf
174
+ if (allConsoleOutput) {
175
+ process.stdout.write(allConsoleOutput);
176
+ }
177
+
178
+ console.log(`[codeflash] Batched runner completed: ${batchCount} batches, ${tests.length} test files, ${totalTimeMs}ms total`);
179
+ }
180
+
181
+ /**
182
+ * Run all test files once (one batch).
183
+ */
184
+ async _runAllTestsOnce(tests, watcher) {
185
+ let hasFailure = false;
186
+ let allConsoleOutput = '';
187
+
188
+ for (const test of tests) {
189
+ if (watcher.isInterrupted()) break;
190
+
191
+ const sendMessageToJest = (eventName, args) => {
192
+ this._eventEmitter.emit(eventName, deepCopy(args));
193
+ };
194
+
195
+ await this._eventEmitter.emit('test-file-start', [test]);
196
+
197
+ try {
198
+ const result = await runTest(
199
+ test.path,
200
+ this._globalConfig,
201
+ test.context.config,
202
+ test.context.resolver,
203
+ this._context,
204
+ sendMessageToJest
205
+ );
206
+
207
+ if (result.console && Array.isArray(result.console)) {
208
+ allConsoleOutput += result.console.map(e => e.message || '').join('\n') + '\n';
209
+ }
210
+
211
+ if (result.numFailingTests > 0) {
212
+ hasFailure = true;
213
+ }
214
+
215
+ await this._eventEmitter.emit('test-file-success', [test, result]);
216
+ } catch (error) {
217
+ hasFailure = true;
218
+ await this._eventEmitter.emit('test-file-failure', [test, error]);
219
+ }
220
+ }
221
+
222
+ return { consoleOutput: allConsoleOutput, hasFailure };
223
+ }
224
+ }
225
+
226
+ module.exports = CodeflashLoopRunner;