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 +17 -4
- package/runtime/capture.js +231 -67
- package/runtime/compare-results.js +3 -1
- package/runtime/index.d.ts +146 -0
- package/runtime/index.js +7 -0
- package/runtime/loop-runner.js +226 -0
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeflash",
|
|
3
|
-
"version": "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
|
-
"
|
|
74
|
-
"better-sqlite3": "^
|
|
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
|
}
|
package/runtime/capture.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
//
|
|
502
|
-
|
|
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;
|
|
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
|
|
527
|
-
const
|
|
632
|
+
// Create unique key for this invocation (identifies this specific capturePerf call site)
|
|
633
|
+
const invocationKey = `${safeModulePath}:${testClassName}:${safeTestFunctionName}:${funcName}:${lineId}`;
|
|
528
634
|
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
const
|
|
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
|
-
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
646
|
+
let lastReturnValue;
|
|
647
|
+
let lastError = null;
|
|
538
648
|
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
//
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
579
|
-
console.log(`!######${testStdoutTag}:${durationNs}######!`);
|
|
721
|
+
if (lastError) throw lastError;
|
|
580
722
|
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
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;
|