codeflash 0.0.1 → 0.2.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/README.md +104 -0
- package/bin/codeflash-setup.js +13 -0
- package/bin/codeflash.js +131 -0
- package/package.json +84 -6
- package/runtime/capture.js +871 -0
- package/runtime/comparator.js +406 -0
- package/runtime/compare-results.js +331 -0
- package/runtime/index.d.ts +146 -0
- package/runtime/index.js +86 -0
- package/runtime/loop-runner.js +226 -0
- package/runtime/serializer.js +851 -0
- package/scripts/postinstall.js +265 -0
- package/index.js +0 -7
|
@@ -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;
|