callman-core 1.36.0 → 1.38.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 (204) hide show
  1. package/dist/assertions/dbAssertions.d.ts +7 -0
  2. package/dist/assertions/dbAssertions.d.ts.map +1 -0
  3. package/dist/assertions/dbAssertions.js +191 -0
  4. package/dist/assertions/dbAssertions.js.map +1 -0
  5. package/dist/assertions/index.d.ts +4 -0
  6. package/dist/assertions/index.d.ts.map +1 -0
  7. package/dist/assertions/index.js +3 -0
  8. package/dist/assertions/index.js.map +1 -0
  9. package/dist/assertions/redisAssertions.d.ts +9 -0
  10. package/dist/assertions/redisAssertions.d.ts.map +1 -0
  11. package/dist/assertions/redisAssertions.js +159 -0
  12. package/dist/assertions/redisAssertions.js.map +1 -0
  13. package/dist/assertions/shared.d.ts +5 -0
  14. package/dist/assertions/shared.d.ts.map +1 -0
  15. package/dist/assertions/shared.js +25 -0
  16. package/dist/assertions/shared.js.map +1 -0
  17. package/dist/assertions/types.d.ts +46 -0
  18. package/dist/assertions/types.d.ts.map +1 -0
  19. package/dist/assertions/types.js +5 -0
  20. package/dist/assertions/types.js.map +1 -0
  21. package/dist/host/connections.d.ts +11 -0
  22. package/dist/host/connections.d.ts.map +1 -1
  23. package/dist/host/connections.js +22 -0
  24. package/dist/host/connections.js.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/loadtest/compare.d.ts +32 -0
  30. package/dist/loadtest/compare.d.ts.map +1 -0
  31. package/dist/loadtest/compare.js +55 -0
  32. package/dist/loadtest/compare.js.map +1 -0
  33. package/dist/loadtest/flow-eval.d.ts +21 -0
  34. package/dist/loadtest/flow-eval.d.ts.map +1 -0
  35. package/dist/loadtest/flow-eval.js +199 -0
  36. package/dist/loadtest/flow-eval.js.map +1 -0
  37. package/dist/loadtest/from-collection.d.ts +51 -0
  38. package/dist/loadtest/from-collection.d.ts.map +1 -0
  39. package/dist/loadtest/from-collection.js +131 -0
  40. package/dist/loadtest/from-collection.js.map +1 -0
  41. package/dist/loadtest/histogram.d.ts +41 -0
  42. package/dist/loadtest/histogram.d.ts.map +1 -0
  43. package/dist/loadtest/histogram.js +166 -0
  44. package/dist/loadtest/histogram.js.map +1 -0
  45. package/dist/loadtest/index.d.ts +18 -0
  46. package/dist/loadtest/index.d.ts.map +1 -0
  47. package/dist/loadtest/index.js +14 -0
  48. package/dist/loadtest/index.js.map +1 -0
  49. package/dist/loadtest/node/index.d.ts +4 -0
  50. package/dist/loadtest/node/index.d.ts.map +1 -0
  51. package/dist/loadtest/node/index.js +4 -0
  52. package/dist/loadtest/node/index.js.map +1 -0
  53. package/dist/loadtest/node/metrics.d.ts +84 -0
  54. package/dist/loadtest/node/metrics.d.ts.map +1 -0
  55. package/dist/loadtest/node/metrics.js +396 -0
  56. package/dist/loadtest/node/metrics.js.map +1 -0
  57. package/dist/loadtest/node/orchestrator.d.ts +24 -0
  58. package/dist/loadtest/node/orchestrator.d.ts.map +1 -0
  59. package/dist/loadtest/node/orchestrator.js +363 -0
  60. package/dist/loadtest/node/orchestrator.js.map +1 -0
  61. package/dist/loadtest/node/protocol.d.ts +102 -0
  62. package/dist/loadtest/node/protocol.d.ts.map +1 -0
  63. package/dist/loadtest/node/protocol.js +17 -0
  64. package/dist/loadtest/node/protocol.js.map +1 -0
  65. package/dist/loadtest/node/setup.d.ts +6 -0
  66. package/dist/loadtest/node/setup.d.ts.map +1 -0
  67. package/dist/loadtest/node/setup.js +100 -0
  68. package/dist/loadtest/node/setup.js.map +1 -0
  69. package/dist/loadtest/node/step-compiler.d.ts +29 -0
  70. package/dist/loadtest/node/step-compiler.d.ts.map +1 -0
  71. package/dist/loadtest/node/step-compiler.js +229 -0
  72. package/dist/loadtest/node/step-compiler.js.map +1 -0
  73. package/dist/loadtest/node/transport.d.ts +66 -0
  74. package/dist/loadtest/node/transport.d.ts.map +1 -0
  75. package/dist/loadtest/node/transport.js +414 -0
  76. package/dist/loadtest/node/transport.js.map +1 -0
  77. package/dist/loadtest/node/worker-core.d.ts +77 -0
  78. package/dist/loadtest/node/worker-core.d.ts.map +1 -0
  79. package/dist/loadtest/node/worker-core.js +744 -0
  80. package/dist/loadtest/node/worker-core.js.map +1 -0
  81. package/dist/loadtest/node/worker.d.ts +2 -0
  82. package/dist/loadtest/node/worker.d.ts.map +1 -0
  83. package/dist/loadtest/node/worker.js +28 -0
  84. package/dist/loadtest/node/worker.js.map +1 -0
  85. package/dist/loadtest/plan-file.d.ts +91 -0
  86. package/dist/loadtest/plan-file.d.ts.map +1 -0
  87. package/dist/loadtest/plan-file.js +134 -0
  88. package/dist/loadtest/plan-file.js.map +1 -0
  89. package/dist/loadtest/plan.d.ts +47 -0
  90. package/dist/loadtest/plan.d.ts.map +1 -0
  91. package/dist/loadtest/plan.js +259 -0
  92. package/dist/loadtest/plan.js.map +1 -0
  93. package/dist/loadtest/report.d.ts +8 -0
  94. package/dist/loadtest/report.d.ts.map +1 -0
  95. package/dist/loadtest/report.js +211 -0
  96. package/dist/loadtest/report.js.map +1 -0
  97. package/dist/loadtest/thresholds.d.ts +55 -0
  98. package/dist/loadtest/thresholds.d.ts.map +1 -0
  99. package/dist/loadtest/thresholds.js +203 -0
  100. package/dist/loadtest/thresholds.js.map +1 -0
  101. package/dist/loadtest/types.d.ts +500 -0
  102. package/dist/loadtest/types.d.ts.map +1 -0
  103. package/dist/loadtest/types.js +8 -0
  104. package/dist/loadtest/types.js.map +1 -0
  105. package/dist/loadtest/validate.d.ts +30 -0
  106. package/dist/loadtest/validate.d.ts.map +1 -0
  107. package/dist/loadtest/validate.js +162 -0
  108. package/dist/loadtest/validate.js.map +1 -0
  109. package/dist/runner/requestAssertions.d.ts +19 -0
  110. package/dist/runner/requestAssertions.d.ts.map +1 -0
  111. package/dist/runner/requestAssertions.js +45 -0
  112. package/dist/runner/requestAssertions.js.map +1 -0
  113. package/dist/runner/requestRunner.d.ts.map +1 -1
  114. package/dist/runner/requestRunner.js +25 -0
  115. package/dist/runner/requestRunner.js.map +1 -1
  116. package/dist/runner/runCollection.d.ts.map +1 -1
  117. package/dist/runner/runCollection.js +4 -1
  118. package/dist/runner/runCollection.js.map +1 -1
  119. package/dist/runner/runRequestWithDataset.d.ts.map +1 -1
  120. package/dist/runner/runRequestWithDataset.js +15 -2
  121. package/dist/runner/runRequestWithDataset.js.map +1 -1
  122. package/dist/templating/compile.d.ts +8 -0
  123. package/dist/templating/compile.d.ts.map +1 -0
  124. package/dist/templating/compile.js +110 -0
  125. package/dist/templating/compile.js.map +1 -0
  126. package/dist/templating/index.d.ts +2 -0
  127. package/dist/templating/index.d.ts.map +1 -1
  128. package/dist/templating/index.js +1 -0
  129. package/dist/templating/index.js.map +1 -1
  130. package/dist/templating/template-engine.d.ts +6 -0
  131. package/dist/templating/template-engine.d.ts.map +1 -1
  132. package/dist/templating/template-engine.js +6 -6
  133. package/dist/templating/template-engine.js.map +1 -1
  134. package/dist/types/index.d.ts +10 -0
  135. package/dist/types/index.d.ts.map +1 -1
  136. package/dist-cjs/assertions/dbAssertions.js +195 -0
  137. package/dist-cjs/assertions/dbAssertions.js.map +1 -0
  138. package/dist-cjs/assertions/index.js +8 -0
  139. package/dist-cjs/assertions/index.js.map +1 -0
  140. package/dist-cjs/assertions/redisAssertions.js +163 -0
  141. package/dist-cjs/assertions/redisAssertions.js.map +1 -0
  142. package/dist-cjs/assertions/shared.js +30 -0
  143. package/dist-cjs/assertions/shared.js.map +1 -0
  144. package/dist-cjs/assertions/types.js +6 -0
  145. package/dist-cjs/assertions/types.js.map +1 -0
  146. package/dist-cjs/host/connections.js +24 -1
  147. package/dist-cjs/host/connections.js.map +1 -1
  148. package/dist-cjs/index.js +2 -0
  149. package/dist-cjs/index.js.map +1 -1
  150. package/dist-cjs/loadtest/compare.js +59 -0
  151. package/dist-cjs/loadtest/compare.js.map +1 -0
  152. package/dist-cjs/loadtest/flow-eval.js +205 -0
  153. package/dist-cjs/loadtest/flow-eval.js.map +1 -0
  154. package/dist-cjs/loadtest/from-collection.js +139 -0
  155. package/dist-cjs/loadtest/from-collection.js.map +1 -0
  156. package/dist-cjs/loadtest/histogram.js +171 -0
  157. package/dist-cjs/loadtest/histogram.js.map +1 -0
  158. package/dist-cjs/loadtest/index.js +78 -0
  159. package/dist-cjs/loadtest/index.js.map +1 -0
  160. package/dist-cjs/loadtest/node/index.js +12 -0
  161. package/dist-cjs/loadtest/node/index.js.map +1 -0
  162. package/dist-cjs/loadtest/node/metrics.js +402 -0
  163. package/dist-cjs/loadtest/node/metrics.js.map +1 -0
  164. package/dist-cjs/loadtest/node/orchestrator.js +372 -0
  165. package/dist-cjs/loadtest/node/orchestrator.js.map +1 -0
  166. package/dist-cjs/loadtest/node/protocol.js +20 -0
  167. package/dist-cjs/loadtest/node/protocol.js.map +1 -0
  168. package/dist-cjs/loadtest/node/setup.js +105 -0
  169. package/dist-cjs/loadtest/node/setup.js.map +1 -0
  170. package/dist-cjs/loadtest/node/step-compiler.js +234 -0
  171. package/dist-cjs/loadtest/node/step-compiler.js.map +1 -0
  172. package/dist-cjs/loadtest/node/transport.js +422 -0
  173. package/dist-cjs/loadtest/node/transport.js.map +1 -0
  174. package/dist-cjs/loadtest/node/worker-core.js +749 -0
  175. package/dist-cjs/loadtest/node/worker-core.js.map +1 -0
  176. package/dist-cjs/loadtest/node/worker.js +30 -0
  177. package/dist-cjs/loadtest/node/worker.js.map +1 -0
  178. package/dist-cjs/loadtest/plan-file.js +143 -0
  179. package/dist-cjs/loadtest/plan-file.js.map +1 -0
  180. package/dist-cjs/loadtest/plan.js +271 -0
  181. package/dist-cjs/loadtest/plan.js.map +1 -0
  182. package/dist-cjs/loadtest/report.js +220 -0
  183. package/dist-cjs/loadtest/report.js.map +1 -0
  184. package/dist-cjs/loadtest/thresholds.js +213 -0
  185. package/dist-cjs/loadtest/thresholds.js.map +1 -0
  186. package/dist-cjs/loadtest/types.js +9 -0
  187. package/dist-cjs/loadtest/types.js.map +1 -0
  188. package/dist-cjs/loadtest/validate.js +169 -0
  189. package/dist-cjs/loadtest/validate.js.map +1 -0
  190. package/dist-cjs/runner/requestAssertions.js +49 -0
  191. package/dist-cjs/runner/requestAssertions.js.map +1 -0
  192. package/dist-cjs/runner/requestRunner.js +25 -0
  193. package/dist-cjs/runner/requestRunner.js.map +1 -1
  194. package/dist-cjs/runner/runCollection.js +4 -1
  195. package/dist-cjs/runner/runCollection.js.map +1 -1
  196. package/dist-cjs/runner/runRequestWithDataset.js +15 -2
  197. package/dist-cjs/runner/runRequestWithDataset.js.map +1 -1
  198. package/dist-cjs/templating/compile.js +114 -0
  199. package/dist-cjs/templating/compile.js.map +1 -0
  200. package/dist-cjs/templating/index.js +3 -1
  201. package/dist-cjs/templating/index.js.map +1 -1
  202. package/dist-cjs/templating/template-engine.js +14 -12
  203. package/dist-cjs/templating/template-engine.js.map +1 -1
  204. 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