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