callman-core 1.36.0 → 1.37.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.
Files changed (145) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/loadtest/compare.d.ts +32 -0
  6. package/dist/loadtest/compare.d.ts.map +1 -0
  7. package/dist/loadtest/compare.js +55 -0
  8. package/dist/loadtest/compare.js.map +1 -0
  9. package/dist/loadtest/flow-eval.d.ts +21 -0
  10. package/dist/loadtest/flow-eval.d.ts.map +1 -0
  11. package/dist/loadtest/flow-eval.js +199 -0
  12. package/dist/loadtest/flow-eval.js.map +1 -0
  13. package/dist/loadtest/from-collection.d.ts +51 -0
  14. package/dist/loadtest/from-collection.d.ts.map +1 -0
  15. package/dist/loadtest/from-collection.js +131 -0
  16. package/dist/loadtest/from-collection.js.map +1 -0
  17. package/dist/loadtest/histogram.d.ts +41 -0
  18. package/dist/loadtest/histogram.d.ts.map +1 -0
  19. package/dist/loadtest/histogram.js +166 -0
  20. package/dist/loadtest/histogram.js.map +1 -0
  21. package/dist/loadtest/index.d.ts +18 -0
  22. package/dist/loadtest/index.d.ts.map +1 -0
  23. package/dist/loadtest/index.js +14 -0
  24. package/dist/loadtest/index.js.map +1 -0
  25. package/dist/loadtest/node/index.d.ts +4 -0
  26. package/dist/loadtest/node/index.d.ts.map +1 -0
  27. package/dist/loadtest/node/index.js +4 -0
  28. package/dist/loadtest/node/index.js.map +1 -0
  29. package/dist/loadtest/node/metrics.d.ts +84 -0
  30. package/dist/loadtest/node/metrics.d.ts.map +1 -0
  31. package/dist/loadtest/node/metrics.js +396 -0
  32. package/dist/loadtest/node/metrics.js.map +1 -0
  33. package/dist/loadtest/node/orchestrator.d.ts +24 -0
  34. package/dist/loadtest/node/orchestrator.d.ts.map +1 -0
  35. package/dist/loadtest/node/orchestrator.js +363 -0
  36. package/dist/loadtest/node/orchestrator.js.map +1 -0
  37. package/dist/loadtest/node/protocol.d.ts +102 -0
  38. package/dist/loadtest/node/protocol.d.ts.map +1 -0
  39. package/dist/loadtest/node/protocol.js +17 -0
  40. package/dist/loadtest/node/protocol.js.map +1 -0
  41. package/dist/loadtest/node/setup.d.ts +6 -0
  42. package/dist/loadtest/node/setup.d.ts.map +1 -0
  43. package/dist/loadtest/node/setup.js +100 -0
  44. package/dist/loadtest/node/setup.js.map +1 -0
  45. package/dist/loadtest/node/step-compiler.d.ts +29 -0
  46. package/dist/loadtest/node/step-compiler.d.ts.map +1 -0
  47. package/dist/loadtest/node/step-compiler.js +229 -0
  48. package/dist/loadtest/node/step-compiler.js.map +1 -0
  49. package/dist/loadtest/node/transport.d.ts +66 -0
  50. package/dist/loadtest/node/transport.d.ts.map +1 -0
  51. package/dist/loadtest/node/transport.js +414 -0
  52. package/dist/loadtest/node/transport.js.map +1 -0
  53. package/dist/loadtest/node/worker-core.d.ts +77 -0
  54. package/dist/loadtest/node/worker-core.d.ts.map +1 -0
  55. package/dist/loadtest/node/worker-core.js +744 -0
  56. package/dist/loadtest/node/worker-core.js.map +1 -0
  57. package/dist/loadtest/node/worker.d.ts +2 -0
  58. package/dist/loadtest/node/worker.d.ts.map +1 -0
  59. package/dist/loadtest/node/worker.js +28 -0
  60. package/dist/loadtest/node/worker.js.map +1 -0
  61. package/dist/loadtest/plan-file.d.ts +91 -0
  62. package/dist/loadtest/plan-file.d.ts.map +1 -0
  63. package/dist/loadtest/plan-file.js +134 -0
  64. package/dist/loadtest/plan-file.js.map +1 -0
  65. package/dist/loadtest/plan.d.ts +47 -0
  66. package/dist/loadtest/plan.d.ts.map +1 -0
  67. package/dist/loadtest/plan.js +259 -0
  68. package/dist/loadtest/plan.js.map +1 -0
  69. package/dist/loadtest/report.d.ts +8 -0
  70. package/dist/loadtest/report.d.ts.map +1 -0
  71. package/dist/loadtest/report.js +211 -0
  72. package/dist/loadtest/report.js.map +1 -0
  73. package/dist/loadtest/thresholds.d.ts +55 -0
  74. package/dist/loadtest/thresholds.d.ts.map +1 -0
  75. package/dist/loadtest/thresholds.js +203 -0
  76. package/dist/loadtest/thresholds.js.map +1 -0
  77. package/dist/loadtest/types.d.ts +500 -0
  78. package/dist/loadtest/types.d.ts.map +1 -0
  79. package/dist/loadtest/types.js +8 -0
  80. package/dist/loadtest/types.js.map +1 -0
  81. package/dist/loadtest/validate.d.ts +30 -0
  82. package/dist/loadtest/validate.d.ts.map +1 -0
  83. package/dist/loadtest/validate.js +162 -0
  84. package/dist/loadtest/validate.js.map +1 -0
  85. package/dist/templating/compile.d.ts +8 -0
  86. package/dist/templating/compile.d.ts.map +1 -0
  87. package/dist/templating/compile.js +110 -0
  88. package/dist/templating/compile.js.map +1 -0
  89. package/dist/templating/index.d.ts +2 -0
  90. package/dist/templating/index.d.ts.map +1 -1
  91. package/dist/templating/index.js +1 -0
  92. package/dist/templating/index.js.map +1 -1
  93. package/dist/templating/template-engine.d.ts +6 -0
  94. package/dist/templating/template-engine.d.ts.map +1 -1
  95. package/dist/templating/template-engine.js +6 -6
  96. package/dist/templating/template-engine.js.map +1 -1
  97. package/dist-cjs/index.js +1 -0
  98. package/dist-cjs/index.js.map +1 -1
  99. package/dist-cjs/loadtest/compare.js +59 -0
  100. package/dist-cjs/loadtest/compare.js.map +1 -0
  101. package/dist-cjs/loadtest/flow-eval.js +205 -0
  102. package/dist-cjs/loadtest/flow-eval.js.map +1 -0
  103. package/dist-cjs/loadtest/from-collection.js +139 -0
  104. package/dist-cjs/loadtest/from-collection.js.map +1 -0
  105. package/dist-cjs/loadtest/histogram.js +171 -0
  106. package/dist-cjs/loadtest/histogram.js.map +1 -0
  107. package/dist-cjs/loadtest/index.js +78 -0
  108. package/dist-cjs/loadtest/index.js.map +1 -0
  109. package/dist-cjs/loadtest/node/index.js +12 -0
  110. package/dist-cjs/loadtest/node/index.js.map +1 -0
  111. package/dist-cjs/loadtest/node/metrics.js +402 -0
  112. package/dist-cjs/loadtest/node/metrics.js.map +1 -0
  113. package/dist-cjs/loadtest/node/orchestrator.js +372 -0
  114. package/dist-cjs/loadtest/node/orchestrator.js.map +1 -0
  115. package/dist-cjs/loadtest/node/protocol.js +20 -0
  116. package/dist-cjs/loadtest/node/protocol.js.map +1 -0
  117. package/dist-cjs/loadtest/node/setup.js +105 -0
  118. package/dist-cjs/loadtest/node/setup.js.map +1 -0
  119. package/dist-cjs/loadtest/node/step-compiler.js +234 -0
  120. package/dist-cjs/loadtest/node/step-compiler.js.map +1 -0
  121. package/dist-cjs/loadtest/node/transport.js +422 -0
  122. package/dist-cjs/loadtest/node/transport.js.map +1 -0
  123. package/dist-cjs/loadtest/node/worker-core.js +749 -0
  124. package/dist-cjs/loadtest/node/worker-core.js.map +1 -0
  125. package/dist-cjs/loadtest/node/worker.js +30 -0
  126. package/dist-cjs/loadtest/node/worker.js.map +1 -0
  127. package/dist-cjs/loadtest/plan-file.js +143 -0
  128. package/dist-cjs/loadtest/plan-file.js.map +1 -0
  129. package/dist-cjs/loadtest/plan.js +271 -0
  130. package/dist-cjs/loadtest/plan.js.map +1 -0
  131. package/dist-cjs/loadtest/report.js +220 -0
  132. package/dist-cjs/loadtest/report.js.map +1 -0
  133. package/dist-cjs/loadtest/thresholds.js +213 -0
  134. package/dist-cjs/loadtest/thresholds.js.map +1 -0
  135. package/dist-cjs/loadtest/types.js +9 -0
  136. package/dist-cjs/loadtest/types.js.map +1 -0
  137. package/dist-cjs/loadtest/validate.js +169 -0
  138. package/dist-cjs/loadtest/validate.js.map +1 -0
  139. package/dist-cjs/templating/compile.js +114 -0
  140. package/dist-cjs/templating/compile.js.map +1 -0
  141. package/dist-cjs/templating/index.js +3 -1
  142. package/dist-cjs/templating/index.js.map +1 -1
  143. package/dist-cjs/templating/template-engine.js +14 -12
  144. package/dist-cjs/templating/template-engine.js.map +1 -1
  145. package/package.json +10 -1
@@ -0,0 +1,749 @@
1
+ "use strict";
2
+ // One load worker: owns a share of the virtual users (closed model) or of the
3
+ // iteration arrivals (open model), drives them over its own HTTP pool, and
4
+ // reports compact per-interval deltas to the orchestrator.
5
+ //
6
+ // The same class runs inside a `worker_threads` Worker (see worker.ts) and
7
+ // inline on the calling thread (small runs, tests, hosts that can't resolve a
8
+ // worker file) — the orchestrator can't tell the difference.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LoadWorker = exports.createSharedCounterStore = void 0;
11
+ const node_perf_hooks_1 = require("node:perf_hooks");
12
+ const executeRequest_js_1 = require("../../request/executeRequest.js");
13
+ const compile_js_1 = require("../../templating/compile.js");
14
+ const template_engine_js_1 = require("../../templating/template-engine.js");
15
+ const histogram_js_1 = require("../histogram.js");
16
+ const plan_js_1 = require("../plan.js");
17
+ const flow_eval_js_1 = require("../flow-eval.js");
18
+ const step_compiler_js_1 = require("./step-compiler.js");
19
+ const transport_js_1 = require("./transport.js");
20
+ const FLUSH_INTERVAL_MS = 500;
21
+ const SLOWEST_KEEP = 20;
22
+ const MONITOR_CAP_PER_INTERVAL = 40;
23
+ const MONITOR_BODY_LIMIT = 64 * 1024;
24
+ const CAPTURE_BODY_LIMIT = 1024 * 1024;
25
+ const DEFAULT_OK = { min: 200, max: 399 };
26
+ /**
27
+ * Arrivals are dispatched from a 2 ms timer, so an iteration due a moment ago
28
+ * legitimately starts up to a couple of milliseconds late. That is generator
29
+ * granularity, not a stalled server, so it is not charged to the latency; a
30
+ * real stall (the thing coordinated-omission correction exists for) is far
31
+ * larger than this.
32
+ */
33
+ const SCHEDULER_TOLERANCE_MS = 6;
34
+ const LEGACY_FAKER_ALIASES = {
35
+ $randomUuid: "$randomUUID",
36
+ $randomString: "random.string(12)",
37
+ $randomInt: "random.int(0,1000000)",
38
+ };
39
+ class Interval {
40
+ requests = 0;
41
+ failed = 0;
42
+ networkErrors = 0;
43
+ iterations = 0;
44
+ dropped = 0;
45
+ bytesSent = 0;
46
+ bytesReceived = 0;
47
+ checksPassed = 0;
48
+ checksFailed = 0;
49
+ newConnections = 0;
50
+ blockedRequests = 0;
51
+ statusClass = { c2: 0, c3: 0, c4: 0, c5: 0, c0: 0 };
52
+ statusCodes = {};
53
+ duration = new histogram_js_1.LatencyHistogram();
54
+ corrected = new histogram_js_1.LatencyHistogram();
55
+ waiting = new histogram_js_1.LatencyHistogram();
56
+ receiving = new histogram_js_1.LatencyHistogram();
57
+ sending = new histogram_js_1.LatencyHistogram();
58
+ blocked = new histogram_js_1.LatencyHistogram();
59
+ connecting = new histogram_js_1.LatencyHistogram();
60
+ tls = new histogram_js_1.LatencyHistogram();
61
+ iteration = new histogram_js_1.LatencyHistogram();
62
+ steps = new Map();
63
+ errors = new Map();
64
+ step(id) {
65
+ let entry = this.steps.get(id);
66
+ if (!entry) {
67
+ entry = { requests: 0, failed: 0, checksPassed: 0, checksFailed: 0, checks: {}, statusCodes: {}, duration: new histogram_js_1.LatencyHistogram() };
68
+ this.steps.set(id, entry);
69
+ }
70
+ return entry;
71
+ }
72
+ }
73
+ const sleep = (ms, signal) => new Promise((resolve) => {
74
+ if (ms <= 0 || signal.aborted) {
75
+ resolve();
76
+ return;
77
+ }
78
+ const timer = setTimeout(() => {
79
+ signal.removeEventListener("abort", onAbort);
80
+ resolve();
81
+ }, ms);
82
+ const onAbort = () => {
83
+ clearTimeout(timer);
84
+ resolve();
85
+ };
86
+ signal.addEventListener("abort", onAbort, { once: true });
87
+ });
88
+ const randomBetween = (min, max) => max <= min ? Math.max(0, min) : Math.floor(min + Math.random() * (max - min + 1));
89
+ const yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
90
+ const createSharedCounterStore = (shared, slots) => {
91
+ const cells = new BigInt64Array(shared);
92
+ const local = new Map();
93
+ return {
94
+ getNextValue: (key, startAt = 1) => {
95
+ const slot = slots[key];
96
+ if (slot)
97
+ return Number(Atomics.add(cells, slot.slot, 1n)) + 1 + (slot.start - 1);
98
+ const next = (local.get(key) ?? startAt - 1) + 1;
99
+ local.set(key, next);
100
+ return next;
101
+ },
102
+ snapshot: () => [],
103
+ hydrate: () => undefined,
104
+ reset: () => undefined,
105
+ };
106
+ };
107
+ exports.createSharedCounterStore = createSharedCounterStore;
108
+ class LoadWorker {
109
+ init;
110
+ emit;
111
+ plan;
112
+ transport;
113
+ controller = new AbortController();
114
+ counters;
115
+ counterStore;
116
+ iterationSteps;
117
+ baseVars;
118
+ oauth;
119
+ lag = (0, node_perf_hooks_1.monitorEventLoopDelay)({ resolution: 10 });
120
+ datasetRows;
121
+ fakerBindings = [];
122
+ okRange;
123
+ monitorEnabled;
124
+ monitorEvery;
125
+ cookieJar;
126
+ timeoutMs;
127
+ maxRedirects;
128
+ graceMs;
129
+ correct;
130
+ maxShare;
131
+ startPerf = 0;
132
+ interval = new Interval();
133
+ flushTimer = null;
134
+ scheduleTimer = null;
135
+ ending = false;
136
+ stopRequested = false;
137
+ hardDeadline = Number.POSITIVE_INFINITY;
138
+ finished = false;
139
+ resolveFinished = null;
140
+ vus = [];
141
+ freeVus = [];
142
+ nextVuSeq = 0;
143
+ inflight = 0;
144
+ issued = 0;
145
+ slowest = [];
146
+ monitorBuffer = [];
147
+ oauthCache = new Map();
148
+ constructor(init, emit) {
149
+ this.init = init;
150
+ this.emit = emit;
151
+ const config = init.config;
152
+ this.plan = (0, plan_js_1.buildLoadPlan)(config.executor);
153
+ this.transport = new transport_js_1.LoadTransport({
154
+ keepAlive: config.keepAlive !== false,
155
+ verifySsl: config.verifySsl !== false,
156
+ clientCertificate: config.clientCertificate,
157
+ });
158
+ this.counters = new BigInt64Array(init.shared);
159
+ this.counterStore = (0, exports.createSharedCounterStore)(init.shared, init.counterSlots);
160
+ const userAgent = config.userAgent ?? executeRequest_js_1.DEFAULT_USER_AGENT;
161
+ this.iterationSteps = config.steps.filter((step) => step.phase !== "setup").map((step) => (0, step_compiler_js_1.compileStep)(step, { userAgent }));
162
+ this.baseVars = { ...(config.environment ?? {}), ...init.setupVars };
163
+ this.okRange = config.okStatus ?? DEFAULT_OK;
164
+ this.monitorEnabled = config.monitor?.enabled === true;
165
+ this.monitorEvery = Math.max(1, config.monitor?.sampleEvery ?? 50);
166
+ this.cookieJar = config.cookieJar !== false;
167
+ this.timeoutMs = config.timeoutMs ?? 30_000;
168
+ this.maxRedirects = config.maxRedirects ?? 5;
169
+ this.graceMs = config.gracefulStopMs ?? 5_000;
170
+ this.correct = config.correctCoordinatedOmission !== false && this.plan.kind === "rate";
171
+ this.maxShare = Math.max(1, Math.ceil(this.plan.maxVus / init.count));
172
+ this.datasetRows = init.dataset;
173
+ if (init.dataset?.kind === "fakers") {
174
+ for (const binding of init.dataset.bindings) {
175
+ let expression = binding.generator.trim().replace(/^\{\{\s*|\s*\}\}$/g, "");
176
+ expression = LEGACY_FAKER_ALIASES[expression] ?? expression;
177
+ this.fakerBindings.push({ name: binding.name, compiled: (0, compile_js_1.compileTemplate)(`{{${expression}}}`) });
178
+ }
179
+ }
180
+ this.oauth = {
181
+ cache: this.oauthCache,
182
+ post: async (request) => {
183
+ const result = await this.transport.send({
184
+ url: request.url,
185
+ method: request.method,
186
+ headers: request.headers,
187
+ body: request.body,
188
+ timeoutMs: this.timeoutMs,
189
+ captureBody: true,
190
+ captureOnError: false,
191
+ maxBodyBytes: CAPTURE_BODY_LIMIT,
192
+ maxRedirects: 0,
193
+ signal: this.controller.signal,
194
+ });
195
+ let data = result.bodyText;
196
+ try {
197
+ data = result.bodyText ? JSON.parse(result.bodyText) : null;
198
+ }
199
+ catch {
200
+ // keep text
201
+ }
202
+ return { status: result.status, data };
203
+ },
204
+ };
205
+ }
206
+ /** Start generating load; resolves when this worker has drained and reported `done`. */
207
+ run() {
208
+ this.lag.enable();
209
+ this.startPerf = node_perf_hooks_1.performance.now() + (this.init.startAtEpochMs - Date.now());
210
+ const finished = new Promise((resolve) => {
211
+ this.resolveFinished = resolve;
212
+ });
213
+ this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
214
+ if (this.plan.kind === "vus")
215
+ this.scheduleVus();
216
+ else
217
+ this.scheduleArrivals();
218
+ return finished;
219
+ }
220
+ stop(graceMs) {
221
+ this.stopRequested = true;
222
+ this.ending = true;
223
+ this.hardDeadline = node_perf_hooks_1.performance.now() + Math.max(0, graceMs);
224
+ if (!this.controller.signal.aborted)
225
+ setTimeout(() => this.controller.abort(), Math.max(0, graceMs)).unref?.();
226
+ }
227
+ elapsed() {
228
+ return node_perf_hooks_1.performance.now() - this.startPerf;
229
+ }
230
+ // ── Closed model ───────────────────────────────────────────────────────────
231
+ shareOf(total) {
232
+ const whole = Math.round(total);
233
+ const base = Math.floor(whole / this.init.count);
234
+ return base + (this.init.index < whole % this.init.count ? 1 : 0);
235
+ }
236
+ scheduleVus() {
237
+ const tick = () => {
238
+ if (this.finished)
239
+ return;
240
+ const elapsed = this.elapsed();
241
+ if (!this.ending && elapsed >= this.plan.totalDurationMs)
242
+ this.ending = true;
243
+ if (elapsed >= 0 && !this.ending) {
244
+ const wanted = this.shareOf((0, plan_js_1.targetAt)(this.plan, elapsed));
245
+ const active = this.vus.filter((vu) => !vu.exit).length;
246
+ for (let n = active; n < wanted; n += 1)
247
+ this.spawnVu();
248
+ if (active > wanted) {
249
+ let toRetire = active - wanted;
250
+ for (let i = this.vus.length - 1; i >= 0 && toRetire > 0; i -= 1) {
251
+ if (!this.vus[i].exit) {
252
+ this.vus[i].exit = true;
253
+ toRetire -= 1;
254
+ }
255
+ }
256
+ }
257
+ }
258
+ this.checkDrained();
259
+ if (!this.finished)
260
+ this.scheduleTimer = setTimeout(tick, 50);
261
+ };
262
+ this.scheduleTimer = setTimeout(tick, Math.max(0, -this.elapsed()));
263
+ }
264
+ spawnVu() {
265
+ const seq = this.nextVuSeq;
266
+ this.nextVuSeq += 1;
267
+ const vu = {
268
+ id: seq * this.init.count + this.init.index + 1,
269
+ exit: false,
270
+ running: true,
271
+ iterations: 0,
272
+ vars: {},
273
+ cookies: new Map(),
274
+ };
275
+ this.vus.push(vu);
276
+ void this.vuLoop(vu);
277
+ }
278
+ async vuLoop(vu) {
279
+ const think = this.init.config.thinkTimeMs;
280
+ try {
281
+ while (!vu.exit && !this.ending) {
282
+ const index = Number(Atomics.add(this.counters, 0, 1n));
283
+ await this.runIteration(vu, index, null);
284
+ if (think && think.max > 0 && !this.ending && !vu.exit) {
285
+ await sleep(randomBetween(think.min, think.max), this.controller.signal);
286
+ }
287
+ await yieldToEventLoop();
288
+ }
289
+ }
290
+ finally {
291
+ vu.running = false;
292
+ const at = this.vus.indexOf(vu);
293
+ if (at >= 0)
294
+ this.vus.splice(at, 1);
295
+ }
296
+ }
297
+ // ── Open model ─────────────────────────────────────────────────────────────
298
+ scheduleArrivals() {
299
+ const tick = () => {
300
+ if (this.finished)
301
+ return;
302
+ const elapsed = this.elapsed();
303
+ if (!this.ending && elapsed >= this.plan.totalDurationMs) {
304
+ // Issue whatever was due before the end, then stop arriving.
305
+ this.issueDue(this.plan.totalDurationMs);
306
+ this.ending = true;
307
+ }
308
+ if (elapsed >= 0 && !this.ending)
309
+ this.issueDue(elapsed);
310
+ this.checkDrained();
311
+ if (!this.finished)
312
+ this.scheduleTimer = setTimeout(tick, 2);
313
+ };
314
+ this.scheduleTimer = setTimeout(tick, Math.max(0, -this.elapsed()));
315
+ }
316
+ issueDue(elapsed) {
317
+ const { index, count } = this.init;
318
+ const cumulative = (0, plan_js_1.cumulativeIterationsAt)(this.plan, elapsed);
319
+ const due = cumulative >= index + 1 ? Math.floor((cumulative - (index + 1)) / count) + 1 : 0;
320
+ while (this.issued < due) {
321
+ this.issued += 1;
322
+ const globalNumber = (this.issued - 1) * count + index + 1;
323
+ if (this.inflight >= this.maxShare) {
324
+ this.interval.dropped += 1;
325
+ continue;
326
+ }
327
+ const intended = (0, plan_js_1.intendedStartMs)(this.plan, globalNumber);
328
+ const vu = this.freeVus.pop() ?? this.newPooledVu();
329
+ this.inflight += 1;
330
+ const iterationIndex = Number(Atomics.add(this.counters, 0, 1n));
331
+ void this.runIteration(vu, iterationIndex, intended).finally(() => {
332
+ this.inflight -= 1;
333
+ this.freeVus.push(vu);
334
+ });
335
+ }
336
+ }
337
+ newPooledVu() {
338
+ const seq = this.nextVuSeq;
339
+ this.nextVuSeq += 1;
340
+ return { id: seq * this.init.count + this.init.index + 1, exit: false, running: true, iterations: 0, vars: {}, cookies: new Map() };
341
+ }
342
+ // ── Drain ──────────────────────────────────────────────────────────────────
343
+ idle() {
344
+ return this.plan.kind === "vus" ? this.vus.length === 0 : this.inflight === 0;
345
+ }
346
+ checkDrained() {
347
+ if (this.finished)
348
+ return;
349
+ if (this.ending && node_perf_hooks_1.performance.now() > this.hardDeadline)
350
+ this.controller.abort();
351
+ if (this.ending && this.hardDeadline === Number.POSITIVE_INFINITY) {
352
+ // Natural end: in-flight work gets `graceMs` to finish.
353
+ this.hardDeadline = node_perf_hooks_1.performance.now() + this.graceMs;
354
+ setTimeout(() => this.controller.abort(), this.graceMs).unref?.();
355
+ }
356
+ if (this.ending && this.idle())
357
+ this.finish();
358
+ }
359
+ finish() {
360
+ if (this.finished)
361
+ return;
362
+ this.finished = true;
363
+ if (this.scheduleTimer)
364
+ clearTimeout(this.scheduleTimer);
365
+ if (this.flushTimer)
366
+ clearInterval(this.flushTimer);
367
+ this.flush();
368
+ this.flushMonitor();
369
+ this.lag.disable();
370
+ this.transport.destroy();
371
+ this.emit({ t: "done", worker: this.init.index, slowest: this.slowest });
372
+ this.resolveFinished?.();
373
+ }
374
+ // ── Iteration ──────────────────────────────────────────────────────────────
375
+ nextRow(iterationIndex) {
376
+ const dataset = this.datasetRows;
377
+ if (!dataset)
378
+ return {};
379
+ if (dataset.kind === "rows") {
380
+ if (dataset.rows.length === 0)
381
+ return {};
382
+ return dataset.mode === "random"
383
+ ? dataset.rows[Math.floor(Math.random() * dataset.rows.length)]
384
+ : dataset.rows[iterationIndex % dataset.rows.length];
385
+ }
386
+ const row = {};
387
+ const context = (0, template_engine_js_1.createTemplateRuntimeContext)({}, { counterStore: this.counterStore });
388
+ for (const binding of this.fakerBindings)
389
+ row[binding.name] = binding.compiled(context);
390
+ return row;
391
+ }
392
+ async runIteration(vu, iterationIndex, intendedMs) {
393
+ const startedPerf = node_perf_hooks_1.performance.now();
394
+ const delayMs = intendedMs === null ? 0 : Math.max(0, startedPerf - this.startPerf - intendedMs - SCHEDULER_TOLERANCE_MS);
395
+ const row = this.nextRow(iterationIndex);
396
+ const variables = Object.assign({}, this.baseVars, vu.vars, row);
397
+ for (let stepIndex = 0; stepIndex < this.iterationSteps.length; stepIndex += 1) {
398
+ if (this.controller.signal.aborted)
399
+ break;
400
+ const prepared = this.iterationSteps[stepIndex];
401
+ if (prepared.step.firstIterationOnly && vu.iterations > 0)
402
+ continue;
403
+ const outcome = await this.runStep(vu, prepared, variables, row, iterationIndex, delayMs);
404
+ if (!outcome && prepared.step.stopIterationOnFailure)
405
+ break;
406
+ const pause = prepared.step.thinkTimeMs;
407
+ if (pause && pause.max > 0 && stepIndex < this.iterationSteps.length - 1) {
408
+ await sleep(randomBetween(pause.min, pause.max), this.controller.signal);
409
+ }
410
+ }
411
+ vu.iterations += 1;
412
+ if (!this.controller.signal.aborted) {
413
+ const total = node_perf_hooks_1.performance.now() - startedPerf;
414
+ this.interval.iterations += 1;
415
+ this.interval.iteration.record(total);
416
+ if (this.correct)
417
+ this.interval.corrected.record(total + delayMs);
418
+ }
419
+ }
420
+ cookieHeader(vu, url) {
421
+ if (!this.cookieJar)
422
+ return null;
423
+ let host;
424
+ try {
425
+ host = new URL(url).host;
426
+ }
427
+ catch {
428
+ return null;
429
+ }
430
+ const jar = vu.cookies.get(host);
431
+ if (!jar || jar.size === 0)
432
+ return null;
433
+ return [...jar.entries()].map(([name, value]) => `${name}=${value}`).join("; ");
434
+ }
435
+ storeCookies(vu, url, setCookies) {
436
+ if (!this.cookieJar || setCookies.length === 0)
437
+ return;
438
+ let host;
439
+ try {
440
+ host = new URL(url).host;
441
+ }
442
+ catch {
443
+ return;
444
+ }
445
+ let jar = vu.cookies.get(host);
446
+ if (!jar) {
447
+ jar = new Map();
448
+ vu.cookies.set(host, jar);
449
+ }
450
+ for (const cookie of setCookies) {
451
+ const [pair, ...attributes] = cookie.split(";");
452
+ const eq = pair.indexOf("=");
453
+ if (eq <= 0)
454
+ continue;
455
+ const name = pair.slice(0, eq).trim();
456
+ const value = pair.slice(eq + 1).trim();
457
+ const expired = attributes.some((attribute) => /^\s*max-age\s*=\s*0\s*$/i.test(attribute));
458
+ if (expired || value === "")
459
+ jar.delete(name);
460
+ else
461
+ jar.set(name, value);
462
+ }
463
+ }
464
+ /** Returns whether the step counted as a success. */
465
+ async runStep(vu, prepared, variables, row, iterationIndex, delayMs) {
466
+ const step = prepared.step;
467
+ const context = (0, step_compiler_js_1.newTemplateContext)(variables, this.counterStore);
468
+ let request;
469
+ try {
470
+ request = await prepared.build(context, this.oauth);
471
+ }
472
+ catch (error) {
473
+ this.recordFailure(step.id, step.name, "config", error instanceof Error ? error.message : String(error), prepared);
474
+ return false;
475
+ }
476
+ const cookie = this.cookieHeader(vu, request.url);
477
+ if (cookie && !Object.keys(request.headers).some((key) => key.toLowerCase() === "cookie")) {
478
+ request.headers.Cookie = cookie;
479
+ }
480
+ const sampled = this.monitorEnabled && iterationIndex % this.monitorEvery === 0;
481
+ const atMs = this.elapsed();
482
+ const result = await this.transport.send({
483
+ url: request.url,
484
+ method: request.method,
485
+ headers: request.headers,
486
+ body: request.body,
487
+ timeoutMs: step.timeoutMs ?? this.timeoutMs,
488
+ captureBody: prepared.needsBody || sampled,
489
+ captureOnError: this.monitorEnabled,
490
+ maxBodyBytes: prepared.needsBody ? CAPTURE_BODY_LIMIT : MONITOR_BODY_LIMIT,
491
+ maxRedirects: this.maxRedirects,
492
+ signal: this.controller.signal,
493
+ });
494
+ if (result.error?.class === "aborted")
495
+ return false;
496
+ this.storeCookies(vu, request.url, result.setCookies);
497
+ const status = result.status;
498
+ const networkError = result.error !== undefined;
499
+ const ok = !networkError && (prepared.expectOk ? prepared.expectOk(status) : status >= this.okRange.min && status <= this.okRange.max);
500
+ let checksPassed = 0;
501
+ let checksFailed = 0;
502
+ const stepInterval = this.interval.step(step.id);
503
+ if (!networkError && ((step.extract && step.extract.length > 0) || (step.checks && step.checks.length > 0))) {
504
+ const evaluator = (0, flow_eval_js_1.createResponseEvaluator)({
505
+ status,
506
+ durationMs: result.timings.total,
507
+ headers: result.headers,
508
+ bodyText: result.bodyText,
509
+ });
510
+ if (step.extract?.length) {
511
+ evaluator.extract(step.extract, variables);
512
+ if (step.firstIterationOnly)
513
+ for (const extractor of step.extract)
514
+ if (variables[extractor.variable] !== undefined)
515
+ vu.vars[extractor.variable] = variables[extractor.variable];
516
+ }
517
+ if (step.checks?.length) {
518
+ for (const outcome of evaluator.check(step.checks, prepared.resolve((0, step_compiler_js_1.newTemplateContext)(variables, this.counterStore)))) {
519
+ const slot = (stepInterval.checks[outcome.name] ??= { p: 0, f: 0 });
520
+ if (outcome.passed) {
521
+ slot.p += 1;
522
+ checksPassed += 1;
523
+ }
524
+ else {
525
+ slot.f += 1;
526
+ checksFailed += 1;
527
+ }
528
+ }
529
+ }
530
+ }
531
+ else if (networkError && step.checks?.length) {
532
+ for (const check of step.checks) {
533
+ const slot = (stepInterval.checks[check.name] ??= { p: 0, f: 0 });
534
+ slot.f += 1;
535
+ checksFailed += 1;
536
+ }
537
+ }
538
+ this.recordRequest(prepared, result, ok, checksPassed, checksFailed, delayMs, iterationIndex, vu.id);
539
+ if (!ok || sampled) {
540
+ this.captureMonitor(prepared, request, result, ok, iterationIndex, vu.id, atMs, row);
541
+ }
542
+ return ok;
543
+ }
544
+ // ── Recording ──────────────────────────────────────────────────────────────
545
+ recordFailure(stepId, name, errorClass, message, prepared) {
546
+ void name;
547
+ void prepared;
548
+ const interval = this.interval;
549
+ interval.requests += 1;
550
+ interval.failed += 1;
551
+ interval.networkErrors += 1;
552
+ interval.statusClass.c0 += 1;
553
+ interval.statusCodes["0"] = (interval.statusCodes["0"] ?? 0) + 1;
554
+ const step = interval.step(stepId);
555
+ step.requests += 1;
556
+ step.failed += 1;
557
+ step.statusCodes["0"] = (step.statusCodes["0"] ?? 0) + 1;
558
+ this.noteError(errorClass, message);
559
+ }
560
+ noteError(errorClass, message) {
561
+ const current = this.interval.errors.get(errorClass);
562
+ if (current)
563
+ current.count += 1;
564
+ else
565
+ this.interval.errors.set(errorClass, { count: 1, example: message.slice(0, 300) });
566
+ }
567
+ recordRequest(prepared, result, ok, checksPassed, checksFailed, delayMs, iterationIndex, vuId) {
568
+ const interval = this.interval;
569
+ const status = result.status;
570
+ const timings = result.timings;
571
+ interval.requests += 1;
572
+ interval.bytesSent += result.bytesSent;
573
+ interval.bytesReceived += result.bytesReceived;
574
+ interval.checksPassed += checksPassed;
575
+ interval.checksFailed += checksFailed;
576
+ if (result.newConnection)
577
+ interval.newConnections += 1;
578
+ if (timings.blocked > 5)
579
+ interval.blockedRequests += 1;
580
+ interval.duration.record(timings.total);
581
+ interval.waiting.record(timings.waiting);
582
+ interval.receiving.record(timings.receiving);
583
+ interval.sending.record(timings.sending);
584
+ interval.blocked.record(timings.blocked);
585
+ interval.connecting.record(timings.dns + timings.connecting);
586
+ interval.tls.record(timings.tls);
587
+ if (this.correct)
588
+ interval.corrected.record(timings.total + delayMs);
589
+ const code = String(status);
590
+ interval.statusCodes[code] = (interval.statusCodes[code] ?? 0) + 1;
591
+ if (status === 0)
592
+ interval.statusClass.c0 += 1;
593
+ else if (status < 300)
594
+ interval.statusClass.c2 += 1;
595
+ else if (status < 400)
596
+ interval.statusClass.c3 += 1;
597
+ else if (status < 500)
598
+ interval.statusClass.c4 += 1;
599
+ else
600
+ interval.statusClass.c5 += 1;
601
+ const stepInterval = interval.step(prepared.step.id);
602
+ stepInterval.requests += 1;
603
+ stepInterval.checksPassed += checksPassed;
604
+ stepInterval.checksFailed += checksFailed;
605
+ stepInterval.duration.record(timings.total);
606
+ stepInterval.statusCodes[code] = (stepInterval.statusCodes[code] ?? 0) + 1;
607
+ if (!ok) {
608
+ interval.failed += 1;
609
+ stepInterval.failed += 1;
610
+ if (result.error) {
611
+ interval.networkErrors += 1;
612
+ this.noteError(result.error.class, result.error.message);
613
+ }
614
+ else {
615
+ this.noteError(status >= 500 ? "http_5xx" : "http_4xx", `HTTP ${status} from ${prepared.step.name}`);
616
+ }
617
+ }
618
+ this.trackSlowest(prepared, result, status, iterationIndex, vuId);
619
+ }
620
+ trackSlowest(prepared, result, status, iterationIndex, vuId) {
621
+ const duration = result.timings.total;
622
+ if (this.slowest.length >= SLOWEST_KEEP && duration <= this.slowest[this.slowest.length - 1].durationMs)
623
+ return;
624
+ this.slowest.push({
625
+ iterationIndex,
626
+ vuId,
627
+ stepId: prepared.step.id,
628
+ stepName: prepared.step.name,
629
+ atMs: Math.round(this.elapsed()),
630
+ status,
631
+ durationMs: duration,
632
+ ...(result.error ? { errorMessage: result.error.message } : {}),
633
+ });
634
+ this.slowest.sort((a, b) => b.durationMs - a.durationMs);
635
+ if (this.slowest.length > SLOWEST_KEEP)
636
+ this.slowest.length = SLOWEST_KEEP;
637
+ }
638
+ captureMonitor(prepared, request, result, ok, iterationIndex, vuId, atMs, row) {
639
+ void row;
640
+ if (this.monitorBuffer.length >= MONITOR_CAP_PER_INTERVAL) {
641
+ const okAt = this.monitorBuffer.findIndex((entry) => entry.ok);
642
+ if (okAt >= 0)
643
+ this.monitorBuffer.splice(okAt, 1);
644
+ else if (ok)
645
+ return;
646
+ else
647
+ this.monitorBuffer.shift();
648
+ }
649
+ const body = request.bodyPreview;
650
+ const responseBody = result.bodyText ?? "";
651
+ this.monitorBuffer.push({
652
+ id: `${this.init.config.runId}:${this.init.index}:${iterationIndex}:${prepared.step.id}`,
653
+ iterationIndex,
654
+ vuId,
655
+ stepId: prepared.step.id,
656
+ stepName: prepared.step.name,
657
+ atMs: Math.round(atMs),
658
+ method: request.method,
659
+ url: request.url,
660
+ status: result.status,
661
+ ok,
662
+ durationMs: result.timings.total,
663
+ bytesSent: result.bytesSent,
664
+ bytesReceived: result.bytesReceived,
665
+ ...(result.error ? { errorMessage: result.error.message, errorClass: result.error.class } : {}),
666
+ detail: {
667
+ request: {
668
+ headers: Object.entries(request.headers).map(([key, value]) => ({ key, value })),
669
+ query: request.query.map(([key, value]) => ({ key, value })),
670
+ body: body.length > MONITOR_BODY_LIMIT ? body.slice(0, MONITOR_BODY_LIMIT) : body,
671
+ bodyTruncated: body.length > MONITOR_BODY_LIMIT,
672
+ },
673
+ response: {
674
+ headers: Object.entries(result.headers).map(([key, value]) => ({ key, value })),
675
+ body: responseBody.length > MONITOR_BODY_LIMIT ? responseBody.slice(0, MONITOR_BODY_LIMIT) : responseBody,
676
+ bodyTruncated: result.bodyTruncated || responseBody.length > MONITOR_BODY_LIMIT,
677
+ },
678
+ },
679
+ });
680
+ }
681
+ flushMonitor() {
682
+ if (this.monitorBuffer.length === 0)
683
+ return;
684
+ const records = this.monitorBuffer;
685
+ this.monitorBuffer = [];
686
+ this.emit({ t: "monitor", worker: this.init.index, records });
687
+ }
688
+ flush() {
689
+ const interval = this.interval;
690
+ this.interval = new Interval();
691
+ const steps = {};
692
+ for (const [id, entry] of interval.steps) {
693
+ steps[id] = {
694
+ requests: entry.requests,
695
+ failed: entry.failed,
696
+ checksPassed: entry.checksPassed,
697
+ checksFailed: entry.checksFailed,
698
+ checks: entry.checks,
699
+ statusCodes: entry.statusCodes,
700
+ duration: entry.duration.toSparse(),
701
+ };
702
+ }
703
+ const errors = {};
704
+ for (const [key, value] of interval.errors)
705
+ errors[key] = value;
706
+ const lagMean = Number.isFinite(this.lag.mean) ? this.lag.mean / 1e6 : 0;
707
+ const lagMax = Number.isFinite(this.lag.max) ? this.lag.max / 1e6 : 0;
708
+ this.lag.reset();
709
+ const delta = {
710
+ t: "delta",
711
+ worker: this.init.index,
712
+ atMs: Math.round(this.elapsed()),
713
+ activeVus: this.plan.kind === "vus" ? this.vus.length : this.inflight,
714
+ requests: interval.requests,
715
+ failed: interval.failed,
716
+ networkErrors: interval.networkErrors,
717
+ iterations: interval.iterations,
718
+ dropped: interval.dropped,
719
+ bytesSent: interval.bytesSent,
720
+ bytesReceived: interval.bytesReceived,
721
+ checksPassed: interval.checksPassed,
722
+ checksFailed: interval.checksFailed,
723
+ newConnections: interval.newConnections,
724
+ blockedRequests: interval.blockedRequests,
725
+ statusClass: interval.statusClass,
726
+ statusCodes: interval.statusCodes,
727
+ histograms: {
728
+ duration: interval.duration.toSparse(),
729
+ corrected: interval.corrected.toSparse(),
730
+ waiting: interval.waiting.toSparse(),
731
+ receiving: interval.receiving.toSparse(),
732
+ sending: interval.sending.toSparse(),
733
+ blocked: interval.blocked.toSparse(),
734
+ connecting: interval.connecting.toSparse(),
735
+ tls: interval.tls.toSparse(),
736
+ iteration: interval.iteration.toSparse(),
737
+ },
738
+ steps,
739
+ errors,
740
+ loopLagMeanMs: lagMean,
741
+ loopLagMaxMs: lagMax,
742
+ rssBytes: process.memoryUsage().rss,
743
+ };
744
+ this.emit(delta);
745
+ this.flushMonitor();
746
+ }
747
+ }
748
+ exports.LoadWorker = LoadWorker;
749
+ //# sourceMappingURL=worker-core.js.map