@loadstrike/loadstrike-sdk 1.0.30001 → 1.0.30401

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.
@@ -14,6 +14,10 @@ const correlation_js_1 = require("./correlation.js");
14
14
  const transports_js_1 = require("./transports.js");
15
15
  const reporting_js_1 = require("./reporting.js");
16
16
  const sinks_js_1 = require("./sinks.js");
17
+ const load_engine_v2_js_1 = require("./load-engine-v2.js");
18
+ const iteration_observations_js_1 = require("./iteration-observations.js");
19
+ const LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS = Symbol("loadstrike.load-engine-v2.scheduler-distributions");
20
+ const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
17
21
  exports.LoadStrikeNodeType = {
18
22
  SingleNode: "SingleNode",
19
23
  Coordinator: "Coordinator",
@@ -147,12 +151,20 @@ class LoadStrikePluginData {
147
151
  }
148
152
  exports.LoadStrikePluginData = LoadStrikePluginData;
149
153
  class MeasurementAccumulator {
150
- constructor() {
154
+ constructor(useHistogram = false) {
155
+ this.useHistogram = useHistogram;
151
156
  this.count = 0;
152
157
  this.allBytes = 0;
153
158
  this.latenciesMs = [];
154
159
  this.sizesBytes = [];
160
+ this.latencyLessOrEq800 = 0;
161
+ this.latencyMore800Less1200 = 0;
162
+ this.latencyMoreOrEq1200 = 0;
155
163
  this.statusCodes = new Map();
164
+ if (useHistogram) {
165
+ this.latencyHistogram = new load_engine_v2_js_1.LoadStrikeHistogramV1();
166
+ this.sizeHistogram = new load_engine_v2_js_1.LoadStrikeHistogramV1();
167
+ }
156
168
  }
157
169
  get Count() {
158
170
  return this.count;
@@ -169,8 +181,20 @@ class MeasurementAccumulator {
169
181
  const key = `${statusCode}|${message}|${reply.isSuccess ? "ok" : "fail"}`;
170
182
  this.count += 1;
171
183
  this.allBytes += sizeBytes;
172
- this.latenciesMs.push(latencyMs);
173
- this.sizesBytes.push(sizeBytes);
184
+ if (this.useHistogram) {
185
+ this.latencyHistogram.record(normalizeLatencyMicroseconds(latencyMs));
186
+ this.sizeHistogram.record(normalizeHistogramInteger(sizeBytes, "Response size"));
187
+ }
188
+ else {
189
+ this.latenciesMs.push(latencyMs);
190
+ this.sizesBytes.push(sizeBytes);
191
+ }
192
+ if (latencyMs <= 800)
193
+ this.latencyLessOrEq800 += 1;
194
+ else if (latencyMs < 1200)
195
+ this.latencyMore800Less1200 += 1;
196
+ else
197
+ this.latencyMoreOrEq1200 += 1;
174
198
  const existing = this.statusCodes.get(key);
175
199
  if (existing) {
176
200
  existing.count += 1;
@@ -188,6 +212,9 @@ class MeasurementAccumulator {
188
212
  * Use this when all builder inputs are ready to be materialized.
189
213
  */
190
214
  build(allRequestCount, durationMs) {
215
+ if (this.useHistogram) {
216
+ return buildHistogramMeasurement(this.histogramSnapshot(), allRequestCount, durationMs);
217
+ }
191
218
  const count = this.count;
192
219
  const totalDurationMs = Math.max(durationMs, 0);
193
220
  const latencyValues = [...this.latenciesMs];
@@ -236,14 +263,274 @@ class MeasurementAccumulator {
236
263
  statusCodes
237
264
  };
238
265
  }
266
+ buildCombined(other, allRequestCount, durationMs) {
267
+ if (!this.useHistogram || !other.useHistogram) {
268
+ throw new Error("Combined measurements require Load Engine V2 histograms.");
269
+ }
270
+ const left = this.histogramSnapshot();
271
+ const right = other.histogramSnapshot();
272
+ left.latency.merge(right.latency);
273
+ left.size.merge(right.size);
274
+ for (const [key, value] of right.statusCodes) {
275
+ const existing = left.statusCodes.get(key);
276
+ if (existing)
277
+ existing.count += value.count;
278
+ else
279
+ left.statusCodes.set(key, { ...value });
280
+ }
281
+ return buildHistogramMeasurement({
282
+ count: left.count + right.count,
283
+ allBytes: left.allBytes + right.allBytes,
284
+ latency: left.latency,
285
+ size: left.size,
286
+ statusCodes: left.statusCodes,
287
+ lessOrEq800: left.lessOrEq800 + right.lessOrEq800,
288
+ more800Less1200: left.more800Less1200 + right.more800Less1200,
289
+ moreOrEq1200: left.moreOrEq1200 + right.moreOrEq1200
290
+ }, allRequestCount, durationMs);
291
+ }
292
+ histogramSnapshot() {
293
+ return {
294
+ count: this.count,
295
+ allBytes: this.allBytes,
296
+ latency: this.latencyHistogram.clone(),
297
+ size: this.sizeHistogram.clone(),
298
+ statusCodes: new Map(Array.from(this.statusCodes, ([key, value]) => [key, { ...value }])),
299
+ lessOrEq800: this.latencyLessOrEq800,
300
+ more800Less1200: this.latencyMore800Less1200,
301
+ moreOrEq1200: this.latencyMoreOrEq1200
302
+ };
303
+ }
304
+ }
305
+ function buildHistogramMeasurement(snapshot, allRequestCount, durationMs) {
306
+ const count = snapshot.count;
307
+ const totalDurationMs = Math.max(durationMs, 0);
308
+ const latency = snapshot.latency;
309
+ const size = snapshot.size;
310
+ return {
311
+ count64: latency.count.toString(),
312
+ distributionMode: latency.mode === "quantized-v1" || size.mode === "quantized-v1"
313
+ ? "quantized-v1"
314
+ : "exact-normalized",
315
+ maxRelativeError: Math.max(latency.maxRelativeError, size.maxRelativeError),
316
+ histogramSidecar: {
317
+ latency: latency.toSidecar(),
318
+ size: size.toSidecar(),
319
+ allBytes64: size.exactTotal.toString(),
320
+ lessOrEq80064: snapshot.lessOrEq800.toString(),
321
+ more800Less120064: snapshot.more800Less1200.toString(),
322
+ moreOrEq120064: snapshot.moreOrEq1200.toString()
323
+ },
324
+ request: {
325
+ count,
326
+ percent: allRequestCount <= 0 ? 0 : Math.round((100 * count) / allRequestCount),
327
+ rps: totalDurationMs <= 0 ? 0 : count / (totalDurationMs / 1000)
328
+ },
329
+ dataTransfer: {
330
+ allBytes: snapshot.allBytes,
331
+ allBytes64: size.exactTotal.toString(),
332
+ minBytes: Number(size.minimum),
333
+ maxBytes: Number(size.maximum),
334
+ meanBytes: Math.round(size.mean),
335
+ percent50: Number(size.percentile(0.5)),
336
+ percent75: Number(size.percentile(0.75)),
337
+ percent95: Number(size.percentile(0.95)),
338
+ percent99: Number(size.percentile(0.99)),
339
+ percent100: Number(size.percentile(1)),
340
+ stdDev: size.populationStandardDeviation
341
+ },
342
+ latency: {
343
+ latencyCount: {
344
+ lessOrEq800: snapshot.lessOrEq800,
345
+ more800Less1200: snapshot.more800Less1200,
346
+ moreOrEq1200: snapshot.moreOrEq1200
347
+ },
348
+ minMs: Number(latency.minimum) / 1000,
349
+ maxMs: Number(latency.maximum) / 1000,
350
+ meanMs: latency.mean / 1000,
351
+ percent50: Number(latency.percentile(0.5)) / 1000,
352
+ percent75: Number(latency.percentile(0.75)) / 1000,
353
+ percent95: Number(latency.percentile(0.95)) / 1000,
354
+ percent99: Number(latency.percentile(0.99)) / 1000,
355
+ percent100: Number(latency.percentile(1)) / 1000,
356
+ stdDev: latency.populationStandardDeviation / 1000
357
+ },
358
+ statusCodes: Array.from(snapshot.statusCodes.values())
359
+ .sort((left, right) => right.count - left.count)
360
+ .map((value) => ({
361
+ count: value.count,
362
+ isError: value.isError,
363
+ message: value.message,
364
+ percent: count <= 0 ? 0 : Math.round((100 * value.count) / count),
365
+ statusCode: value.statusCode
366
+ }))
367
+ };
368
+ }
369
+ function normalizeLatencyMicroseconds(latencyMs) {
370
+ return normalizeHistogramInteger(Math.max(latencyMs, 0) * 1000, "Latency");
371
+ }
372
+ function normalizeRawObservationLatencyMicroseconds(latencyMs) {
373
+ const maximum = 9223372036854775807n;
374
+ if (!Number.isFinite(latencyMs) || latencyMs <= 0) {
375
+ return 0n;
376
+ }
377
+ const microseconds = latencyMs * 1000;
378
+ if (!Number.isFinite(microseconds) || microseconds >= Number(maximum)) {
379
+ return maximum;
380
+ }
381
+ return BigInt(Math.max(0, Math.round(microseconds)));
382
+ }
383
+ function normalizeHistogramInteger(value, name) {
384
+ if (!Number.isFinite(value) || value > Number(9223372036854775807n)) {
385
+ throw new RangeError(`${name} is outside the supported histogram range.`);
386
+ }
387
+ return BigInt(Math.max(0, Math.round(value)));
388
+ }
389
+ class LoadEngineV2Telemetry {
390
+ constructor(budget) {
391
+ this.budget = budget;
392
+ this.mutableSegments = [];
393
+ this.warnings = new Map();
394
+ }
395
+ createSegment(scenarioName, scenarioIndex, simulationIndex, kind, shardIndex, shardCount) {
396
+ const segment = {
397
+ scenarioName,
398
+ scenarioIndex,
399
+ simulationIndex,
400
+ kind,
401
+ shardIndex,
402
+ shardCount,
403
+ planned: 0n,
404
+ due: 0n,
405
+ started: 0n,
406
+ completed: 0n,
407
+ dropped: 0n,
408
+ unreached: 0n,
409
+ requestedWorkers: 0n,
410
+ startedWorkers: 0n,
411
+ unavailableWorkers: 0n,
412
+ dropReasons: new Map(),
413
+ unavailableWorkerReasons: new Map(),
414
+ decisionLag: new load_engine_v2_js_1.LoadStrikeHistogramV1(),
415
+ startLag: new load_engine_v2_js_1.LoadStrikeHistogramV1(),
416
+ accountingComplete: false
417
+ };
418
+ this.mutableSegments.push(segment);
419
+ return segment;
420
+ }
421
+ recordWarning(code, segment, count) {
422
+ if (count <= 0n)
423
+ return;
424
+ const key = `${code}\n${segment.scenarioIndex}\n${segment.simulationIndex}`;
425
+ const nowNs = BigInt(Date.now()) * 1000000n;
426
+ const existing = this.warnings.get(key);
427
+ if (existing) {
428
+ existing.count += count;
429
+ existing.lastObservedUtcNs = nowNs;
430
+ }
431
+ else {
432
+ this.warnings.set(key, {
433
+ code,
434
+ scenarioName: segment.scenarioName,
435
+ scenarioIndex: segment.scenarioIndex,
436
+ simulationIndex: segment.simulationIndex,
437
+ simulationKind: segment.kind,
438
+ count,
439
+ firstObservedUtcNs: nowNs,
440
+ lastObservedUtcNs: nowNs
441
+ });
442
+ }
443
+ }
444
+ recordDecisionLag(segment, lagNs) {
445
+ segment.decisionLag.record(maxBigInt(lagNs, 0n) / 1000n);
446
+ }
447
+ recordStartLag(segment, lagNs) {
448
+ segment.startLag.record(maxBigInt(lagNs, 0n) / 1000n);
449
+ }
450
+ buildSchedulerDistributions() {
451
+ return this.mutableSegments.flatMap((segment) => {
452
+ const scenarioIndex64 = segment.scenarioIndex.toString();
453
+ const simulationIndex64 = segment.simulationIndex.toString();
454
+ return [
455
+ ["scheduler-decision-lag", "decision", segment.decisionLag],
456
+ ["scheduler-start-lag", "start", segment.startLag]
457
+ ].map(([seriesKind, identityKind, histogram]) => ({
458
+ seriesKind,
459
+ scenarioIndex64,
460
+ scenarioName: segment.scenarioName,
461
+ identityKeyHex: (0, cluster_js_1.buildLoadEngineV2SchedulerIdentityKey)(identityKind, scenarioIndex64, simulationIndex64).toString("hex"),
462
+ outcome: "none",
463
+ unit: "microseconds",
464
+ histogram: histogram.toSidecar(),
465
+ exactTotalDecimalOrEmpty: histogram.toSidecar().exactTotal64
466
+ }));
467
+ });
468
+ }
469
+ buildWarnings() {
470
+ return Array.from(this.warnings.values())
471
+ .sort((left, right) => left.code.localeCompare(right.code)
472
+ || left.scenarioName.localeCompare(right.scenarioName)
473
+ || left.simulationIndex - right.simulationIndex)
474
+ .map((value) => ({
475
+ code: value.code,
476
+ scenarioName: value.scenarioName,
477
+ scenarioIndex: value.scenarioIndex,
478
+ simulationIndex: value.simulationIndex,
479
+ simulationKind: value.simulationKind,
480
+ count64: value.count.toString(),
481
+ message: value.code,
482
+ firstObservedUtcNs: value.firstObservedUtcNs.toString(),
483
+ lastObservedUtcNs: value.lastObservedUtcNs.toString()
484
+ }));
485
+ }
486
+ buildSegments() {
487
+ return this.mutableSegments.map((segment) => ({
488
+ scenarioName: segment.scenarioName,
489
+ scenarioIndex: segment.scenarioIndex,
490
+ simulationIndex: segment.simulationIndex,
491
+ kind: segment.kind,
492
+ shardIndex: segment.shardIndex,
493
+ shardCount: segment.shardCount,
494
+ plannedIterations64: segment.planned.toString(),
495
+ dueIterations64: segment.due.toString(),
496
+ startedIterations64: segment.started.toString(),
497
+ completedIterations64: segment.completed.toString(),
498
+ droppedIterations64: segment.dropped.toString(),
499
+ unreachedIterations64: segment.unreached.toString(),
500
+ requestedWorkerSlots64: segment.requestedWorkers.toString(),
501
+ startedWorkerSlots64: segment.startedWorkers.toString(),
502
+ unavailableWorkerSlots64: segment.unavailableWorkers.toString(),
503
+ dropReasons: Object.fromEntries(Array.from(segment.dropReasons, ([key, count]) => [key, count.toString()])),
504
+ unavailableWorkerReasons: Object.fromEntries(Array.from(segment.unavailableWorkerReasons, ([key, count]) => [key, count.toString()])),
505
+ deliveryPercent: segment.due === 0n ? 100 : Number(segment.started * 10000n / segment.due) / 100,
506
+ accountingComplete: segment.accountingComplete
507
+ }));
508
+ }
509
+ buildStats() {
510
+ return {
511
+ configuredMaxInFlight: this.budget.maxInFlight,
512
+ maxInFlightObserved: this.budget.highWater,
513
+ currentInFlight: this.budget.current,
514
+ segments: this.buildSegments()
515
+ };
516
+ }
517
+ }
518
+ function incrementReason(reasons, code, count = 1n) {
519
+ reasons.set(code, (reasons.get(code) ?? 0n) + count);
520
+ }
521
+ function ownedV2OrdinalCount(total, shardIndex, shardCount) {
522
+ if (total <= BigInt(shardIndex))
523
+ return 0n;
524
+ return (total - 1n - BigInt(shardIndex)) / BigInt(shardCount) + 1n;
239
525
  }
240
526
  class StepStatsAccumulator {
241
- constructor(scenarioName, stepName, sortIndex) {
527
+ constructor(scenarioName, stepName, sortIndex, useHistogram = false) {
242
528
  this.scenarioName = scenarioName;
243
529
  this.stepName = stepName;
244
530
  this.sortIndex = sortIndex;
245
- this.ok = new MeasurementAccumulator();
246
- this.fail = new MeasurementAccumulator();
531
+ this.useHistogram = useHistogram;
532
+ this.ok = new MeasurementAccumulator(useHistogram);
533
+ this.fail = new MeasurementAccumulator(useHistogram);
247
534
  }
248
535
  /**
249
536
  * Exposes the public record operation.
@@ -282,6 +569,7 @@ class StepStatsAccumulator {
282
569
  minLatencyMs: minCandidates.length ? Math.min(...minCandidates) : 0,
283
570
  maxLatencyMs: maxCandidates.length ? Math.max(...maxCandidates) : 0,
284
571
  statusCodes,
572
+ allMeasurement: this.useHistogram ? this.ok.buildCombined(this.fail, requestCount, durationMs) : undefined,
285
573
  ok,
286
574
  fail,
287
575
  sortIndex: this.sortIndex
@@ -289,15 +577,16 @@ class StepStatsAccumulator {
289
577
  }
290
578
  }
291
579
  class ScenarioStatsAccumulator {
292
- constructor(scenarioName, sortIndex) {
580
+ constructor(scenarioName, sortIndex, useHistogram = false) {
293
581
  this.scenarioName = scenarioName;
294
582
  this.sortIndex = sortIndex;
295
- this.ok = new MeasurementAccumulator();
296
- this.fail = new MeasurementAccumulator();
583
+ this.useHistogram = useHistogram;
297
584
  this.steps = new Map();
298
585
  this.nextStepSortIndex = 0;
299
586
  this.loadSimulationStats = { simulationName: "", value: 0 };
300
587
  this.currentOperation = "None";
588
+ this.ok = new MeasurementAccumulator(useHistogram);
589
+ this.fail = new MeasurementAccumulator(useHistogram);
301
590
  }
302
591
  /**
303
592
  * Exposes the public setLoadSimulation operation.
@@ -338,13 +627,18 @@ class ScenarioStatsAccumulator {
338
627
  * Exposes the public recordStep operation.
339
628
  * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
340
629
  */
341
- recordStep(stepName, reply, observedLatencyMs) {
630
+ recordStep(stepName, reply, observedLatencyMs, sortIndex) {
342
631
  const existing = this.steps.get(stepName);
343
- const step = existing ?? new StepStatsAccumulator(this.scenarioName, stepName, this.nextStepSortIndex += 1);
632
+ const resolvedSortIndex = sortIndex === undefined
633
+ ? this.nextStepSortIndex + 1
634
+ : Math.max(Math.trunc(sortIndex), 0);
635
+ const step = existing ?? new StepStatsAccumulator(this.scenarioName, stepName, resolvedSortIndex, this.useHistogram);
344
636
  if (!existing) {
637
+ this.nextStepSortIndex = Math.max(this.nextStepSortIndex, resolvedSortIndex);
345
638
  this.steps.set(stepName, step);
346
639
  }
347
640
  step.record(reply, observedLatencyMs);
641
+ return step.sortIndex;
348
642
  }
349
643
  /**
350
644
  * Builds the configured payload or helper object.
@@ -373,6 +667,7 @@ class ScenarioStatsAccumulator {
373
667
  minLatencyMs: minCandidates.length ? Math.min(...minCandidates) : 0,
374
668
  maxLatencyMs: maxCandidates.length ? Math.max(...maxCandidates) : 0,
375
669
  statusCodes,
670
+ allMeasurement: this.useHistogram ? this.ok.buildCombined(this.fail, totalRequests, durationMs) : undefined,
376
671
  allBytes,
377
672
  currentOperation: this.currentOperation,
378
673
  durationMs: Math.max(durationMs, 0),
@@ -468,7 +763,8 @@ class LoadStrikeStep {
468
763
  const internal = context;
469
764
  await internal.invokeBeforeStep(stepName);
470
765
  let reply;
471
- const startedAt = Date.now();
766
+ const startedUtcNs = (0, iteration_observations_js_1.utcNowNs)();
767
+ const startedAtNs = process.hrtime.bigint();
472
768
  try {
473
769
  reply = normalizeReply(await run());
474
770
  }
@@ -476,8 +772,21 @@ class LoadStrikeStep {
476
772
  reply = LoadStrikeResponse.fail("step_exception", resolveRuntimeErrorMessage(error, "step failed"), 0);
477
773
  }
478
774
  reply = attachReplyProjection(reply);
479
- const observedLatencyMs = Math.max(Date.now() - startedAt, 0);
480
- internal.recordStep(stepName, reply, observedLatencyMs);
775
+ const observedLatencyNs = maxBigInt(process.hrtime.bigint() - startedAtNs, 0n);
776
+ const completedUtcNs = startedUtcNs + observedLatencyNs;
777
+ const observedLatencyMs = Number(observedLatencyNs) / 1000000;
778
+ const recordedSortIndex = internal.recordStep(stepName, reply, observedLatencyMs);
779
+ internal.recordStepObservation?.((0, iteration_observations_js_1.createIterationStepObservation)({
780
+ stepName,
781
+ sortIndex: typeof recordedSortIndex === "number" ? recordedSortIndex : 0,
782
+ startedUtcNs,
783
+ completedUtcNs,
784
+ observedLatencyUs64: observedLatencyNs / 1000n,
785
+ reportedLatencyUs64: normalizeRawObservationLatencyMicroseconds(resolveRecordedLatency(reply.customLatencyMs, observedLatencyMs)),
786
+ isSuccess: reply.isSuccess,
787
+ statusCode: normalizeStatusCode(reply.statusCode, reply.isSuccess),
788
+ sizeBytes64: normalizeHistogramInteger(Math.max(toNumber(reply.sizeBytes), 0), "Step response bytes")
789
+ }));
481
790
  await internal.invokeAfterStep(stepName, reply);
482
791
  return reply;
483
792
  }
@@ -1016,10 +1325,14 @@ class LoadStrikeContext {
1016
1325
  const normalizedValues = normalizeRunContextCollectionShapes(this.values);
1017
1326
  return {
1018
1327
  displayConsoleMetrics: normalizedValues.ConsoleMetricsEnabled,
1328
+ loadEngineContractVersion: normalizedValues.LoadEngineContractVersion,
1329
+ maxInFlight: normalizedValues.MaxInFlight,
1019
1330
  nodeType: normalizedValues.NodeType,
1020
1331
  localDevClusterEnabled: normalizedValues.LocalDevClusterEnabled,
1021
1332
  agentGroup: normalizedValues.AgentGroup,
1022
1333
  agentsCount: normalizedValues.AgentsCount,
1334
+ agentId: normalizedValues.AgentId,
1335
+ expectedAgentIds: normalizedValues.ExpectedAgentIds,
1023
1336
  targetScenarios: normalizedValues.TargetScenarios,
1024
1337
  agentTargetScenarios: normalizedValues.AgentTargetScenarios,
1025
1338
  coordinatorTargetScenarios: normalizedValues.CoordinatorTargetScenarios,
@@ -1038,6 +1351,13 @@ class LoadStrikeContext {
1038
1351
  reportFolderPath: normalizedValues.ReportFolderPath,
1039
1352
  reportFormats: normalizedValues.ReportFormats,
1040
1353
  reportingIntervalSeconds: normalizedValues.ReportingIntervalSeconds,
1354
+ iterationObservationFlushIntervalSeconds: normalizedValues.IterationObservationFlushIntervalSeconds,
1355
+ maxIterationObservationBufferBytes: normalizedValues.MaxIterationObservationBufferBytes,
1356
+ maxIterationObservationsPerBatch: normalizedValues.MaxIterationObservationsPerBatch,
1357
+ maxIterationObservationBatchBytes: normalizedValues.MaxIterationObservationBatchBytes,
1358
+ iterationObservationSinkQueueDepth: normalizedValues.IterationObservationSinkQueueDepth,
1359
+ iterationObservationSinkParallelism: normalizedValues.IterationObservationSinkParallelism,
1360
+ iterationObservationDrainTimeoutSeconds: normalizedValues.IterationObservationDrainTimeoutSeconds,
1041
1361
  minimumLogLevel: normalizedValues.MinimumLogLevel,
1042
1362
  loggerConfig: normalizedValues.LoggerConfig,
1043
1363
  reportingSinks: normalizedValues.ReportingSinks,
@@ -1051,6 +1371,8 @@ class LoadStrikeContext {
1051
1371
  workerPlugins: normalizedValues.WorkerPlugins,
1052
1372
  customSettings: normalizedValues.CustomSettings,
1053
1373
  globalCustomSettings: normalizedValues.GlobalCustomSettings,
1374
+ clusterShardIndex: normalizedValues.ClusterShardIndex,
1375
+ clusterShardCount: normalizedValues.ClusterShardCount,
1054
1376
  runArgs: this.runArgs.length ? [...this.runArgs] : undefined
1055
1377
  };
1056
1378
  }
@@ -1112,6 +1434,19 @@ class LoadStrikeContext {
1112
1434
  DisplayConsoleMetrics(enable) {
1113
1435
  return this.mergeValues({ ConsoleMetricsEnabled: Boolean(enable) });
1114
1436
  }
1437
+ useLoadEngineV2() {
1438
+ return this.UseLoadEngineV2();
1439
+ }
1440
+ UseLoadEngineV2() {
1441
+ return this.mergeValues({ LoadEngineContractVersion: 2 });
1442
+ }
1443
+ withMaxInFlight(maxInFlight) {
1444
+ return this.WithMaxInFlight(maxInFlight);
1445
+ }
1446
+ WithMaxInFlight(maxInFlight) {
1447
+ validateV2MaxInFlight(this.values.LoadEngineContractVersion, maxInFlight);
1448
+ return this.mergeValues({ MaxInFlight: maxInFlight });
1449
+ }
1115
1450
  /**
1116
1451
  * Toggles local development cluster mode.
1117
1452
  * Use this when you want to simulate coordinator and agent behavior on a single machine.
@@ -1178,6 +1513,26 @@ class LoadStrikeContext {
1178
1513
  WithAgentGroup(agentGroup) {
1179
1514
  return this.mergeValues({ AgentGroup: requireNonEmpty(agentGroup, "Agent group must be provided.") });
1180
1515
  }
1516
+ /** Sets the stable identity required by a remote Load Engine V2 agent. */
1517
+ withAgentId(agentId) {
1518
+ return this.WithAgentId(agentId);
1519
+ }
1520
+ /** Sets the stable identity required by a remote Load Engine V2 agent. */
1521
+ WithAgentId(agentId) {
1522
+ return this.mergeValues({ AgentId: requireNonEmpty(agentId, "Agent id must be provided.") });
1523
+ }
1524
+ /** Sets the exact remote agent identities required by a Load Engine V2 coordinator. */
1525
+ withExpectedAgentIds(...agentIds) {
1526
+ return this.WithExpectedAgentIds(...agentIds);
1527
+ }
1528
+ /** Sets the exact remote agent identities required by a Load Engine V2 coordinator. */
1529
+ WithExpectedAgentIds(...agentIds) {
1530
+ const normalized = validateScenarioNames(agentIds);
1531
+ if (new Set(normalized).size !== normalized.length) {
1532
+ throw new Error("Expected agent ids must be unique.");
1533
+ }
1534
+ return this.mergeValues({ ExpectedAgentIds: normalized });
1535
+ }
1181
1536
  /**
1182
1537
  * Sets the requested agent count.
1183
1538
  * Use this when a coordinator should fan work out across a specific number of agents.
@@ -2022,7 +2377,7 @@ function firstWebVitalViolation(...values) {
2022
2377
  return "";
2023
2378
  }
2024
2379
  class LoadStrikeScenario {
2025
- constructor(name, runHandler, initHandler, cleanHandler, loadSimulations, thresholds, trackingConfiguration, maxFailCount, withoutWarmUpValue, warmUpDurationSeconds, weight, restartIterationOnFail, internalLicenseFeatures = []) {
2380
+ constructor(name, runHandler, initHandler, cleanHandler, loadSimulations, thresholds, trackingConfiguration, maxFailCount, withoutWarmUpValue, warmUpDurationSeconds, weight, restartIterationOnFail, internalLicenseFeatures = [], declaredStepNames = []) {
2026
2381
  this.name = name;
2027
2382
  this.runHandler = runHandler;
2028
2383
  this.initHandler = initHandler;
@@ -2036,6 +2391,7 @@ class LoadStrikeScenario {
2036
2391
  this.weight = weight;
2037
2392
  this.restartIterationOnFail = restartIterationOnFail;
2038
2393
  this.internalLicenseFeatures = normalizeStringArray(internalLicenseFeatures);
2394
+ this.declaredStepNames = normalizeDeclaredStepNames(declaredStepNames);
2039
2395
  }
2040
2396
  static create(name, runHandler) {
2041
2397
  const scenarioName = requireNonEmpty(name, "Scenario name must be provided.");
@@ -2078,7 +2434,7 @@ class LoadStrikeScenario {
2078
2434
  if (typeof handler !== "function") {
2079
2435
  throw new TypeError("Init handler must be provided.");
2080
2436
  }
2081
- return new LoadStrikeScenario(this.name, this.runHandler, handler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures);
2437
+ return new LoadStrikeScenario(this.name, this.runHandler, handler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures, this.declaredStepNames);
2082
2438
  }
2083
2439
  /**
2084
2440
  * Configures init async for this SDK object.
@@ -2095,7 +2451,7 @@ class LoadStrikeScenario {
2095
2451
  if (typeof handler !== "function") {
2096
2452
  throw new TypeError("Clean handler must be provided.");
2097
2453
  }
2098
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, handler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures);
2454
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, handler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures, this.declaredStepNames);
2099
2455
  }
2100
2456
  /**
2101
2457
  * Configures clean async for this SDK object.
@@ -2112,14 +2468,14 @@ class LoadStrikeScenario {
2112
2468
  if (!Number.isFinite(maxFailCount)) {
2113
2469
  throw new RangeError("maxFailCount should be a finite number.");
2114
2470
  }
2115
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, Math.trunc(maxFailCount), this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures);
2471
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, Math.trunc(maxFailCount), this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures, this.declaredStepNames);
2116
2472
  }
2117
2473
  /**
2118
2474
  * Configures out warm up for this SDK object.
2119
2475
  * Use this when out warm up should be set explicitly before the run starts.
2120
2476
  */
2121
2477
  withoutWarmUp() {
2122
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, true, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures);
2478
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, true, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures, this.declaredStepNames);
2123
2479
  }
2124
2480
  /**
2125
2481
  * Configures warm up duration for this SDK object.
@@ -2129,7 +2485,7 @@ class LoadStrikeScenario {
2129
2485
  if (!Number.isFinite(durationSeconds)) {
2130
2486
  throw new RangeError("Warmup duration should be a finite number.");
2131
2487
  }
2132
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, durationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures);
2488
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, durationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures, this.declaredStepNames);
2133
2489
  }
2134
2490
  /**
2135
2491
  * Configures weight for this SDK object.
@@ -2139,14 +2495,14 @@ class LoadStrikeScenario {
2139
2495
  if (!Number.isFinite(weight)) {
2140
2496
  throw new RangeError("Weight should be a finite number.");
2141
2497
  }
2142
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, Math.trunc(weight), this.restartIterationOnFail, this.internalLicenseFeatures);
2498
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, Math.trunc(weight), this.restartIterationOnFail, this.internalLicenseFeatures, this.declaredStepNames);
2143
2499
  }
2144
2500
  /**
2145
2501
  * Configures restart iteration on fail for this SDK object.
2146
2502
  * Use this when restart iteration on fail should be set explicitly before the run starts.
2147
2503
  */
2148
2504
  withRestartIterationOnFail(shouldRestart) {
2149
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, Boolean(shouldRestart), this.internalLicenseFeatures);
2505
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, Boolean(shouldRestart), this.internalLicenseFeatures, this.declaredStepNames);
2150
2506
  }
2151
2507
  /**
2152
2508
  * Configures cross platform tracking for this SDK object.
@@ -2171,7 +2527,7 @@ class LoadStrikeScenario {
2171
2527
  if (isCorrelateExistingTrafficTracking(copied) && this.loadSimulations.length > 0) {
2172
2528
  throw new Error("CorrelateExistingTraffic uses ForDuration and cannot be combined with WithLoadSimulations.");
2173
2529
  }
2174
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, copied, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures);
2530
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, copied, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures, this.declaredStepNames);
2175
2531
  }
2176
2532
  /**
2177
2533
  * Configures load simulations for this SDK object.
@@ -2184,7 +2540,7 @@ class LoadStrikeScenario {
2184
2540
  if (isCorrelateExistingTrafficTracking(this.trackingConfiguration)) {
2185
2541
  throw new Error("CorrelateExistingTraffic uses ForDuration and cannot be combined with WithLoadSimulations.");
2186
2542
  }
2187
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, simulations.map((simulation) => attachLoadSimulationProjection({ ...simulation })), this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures);
2543
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, simulations.map((simulation) => attachLoadSimulationProjection({ ...simulation })), this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures, this.declaredStepNames);
2188
2544
  }
2189
2545
  /**
2190
2546
  * Configures thresholds for this SDK object.
@@ -2194,7 +2550,7 @@ class LoadStrikeScenario {
2194
2550
  if (!thresholds.length) {
2195
2551
  throw new Error("At least one threshold should be provided.");
2196
2552
  }
2197
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, thresholds.map((threshold) => ({ ...threshold })), this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures);
2553
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, thresholds.map((threshold) => ({ ...threshold })), this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures, this.declaredStepNames);
2198
2554
  }
2199
2555
  /**
2200
2556
  * Returns simulations.
@@ -2255,6 +2611,26 @@ class LoadStrikeScenario {
2255
2611
  __loadStrikeInternalLicenseFeatures() {
2256
2612
  return [...this.internalLicenseFeatures];
2257
2613
  }
2614
+ /** Freezes the named step identities that may be reported by this scenario. */
2615
+ withDeclaredSteps(...stepNames) {
2616
+ if (!stepNames.length) {
2617
+ throw new Error("At least one declared step name should be provided.");
2618
+ }
2619
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.internalLicenseFeatures, stepNames);
2620
+ }
2621
+ /** Returns the immutable declared-step names in declaration order. */
2622
+ getDeclaredSteps() {
2623
+ return [...this.declaredStepNames];
2624
+ }
2625
+ __loadStrikeSetTrafficMixV2Metadata(metadata) {
2626
+ this.trafficMixV2Metadata = cloneLoadEngineV2TrafficMixMetadata(metadata);
2627
+ return this;
2628
+ }
2629
+ __loadStrikeTrafficMixV2Metadata() {
2630
+ return this.trafficMixV2Metadata
2631
+ ? cloneLoadEngineV2TrafficMixMetadata(this.trafficMixV2Metadata)
2632
+ : undefined;
2633
+ }
2258
2634
  __loadStrikeScenarioSourceAnalysis() {
2259
2635
  const source = this.runHandler.toString();
2260
2636
  const lines = source
@@ -2277,7 +2653,7 @@ class LoadStrikeScenario {
2277
2653
  }
2278
2654
  __loadStrikeWithInternalLicenseFeatures(...features) {
2279
2655
  const merged = Array.from(new Set([...this.internalLicenseFeatures, ...normalizeStringArray(features)]));
2280
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, merged);
2656
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, merged, this.declaredStepNames);
2281
2657
  }
2282
2658
  async invokeInit(context) {
2283
2659
  if (this.initHandler) {
@@ -2298,6 +2674,9 @@ class LoadStrikeScenario {
2298
2674
  return normalizeReply(result);
2299
2675
  }
2300
2676
  catch (error) {
2677
+ if (error instanceof RuntimePolicyCallbackError) {
2678
+ throw error;
2679
+ }
2301
2680
  return LoadStrikeResponse.fail("exception", resolveRuntimeErrorMessage(error, "scenario failed"), 0);
2302
2681
  }
2303
2682
  }
@@ -2343,6 +2722,10 @@ class LoadStrikeScenario {
2343
2722
  WithLoadSimulations(...simulations) {
2344
2723
  return this.withLoadSimulations(...simulations);
2345
2724
  }
2725
+ /** Freezes the named step identities that may be reported by this scenario. */
2726
+ WithDeclaredSteps(...stepNames) {
2727
+ return this.withDeclaredSteps(...stepNames);
2728
+ }
2346
2729
  /**
2347
2730
  * Configures max fail count for this SDK object.
2348
2731
  * Use this when max fail count should be set explicitly before the run starts.
@@ -2473,7 +2856,7 @@ class LoadStrikeTrafficMix {
2473
2856
  return this.withScenarioMix(...scenarioMix);
2474
2857
  }
2475
2858
  expandScenarios() {
2476
- return expandTrafficMixScenarios(this);
2859
+ return expandTrafficMixScenarios(this, 0);
2477
2860
  }
2478
2861
  ExpandScenarios() {
2479
2862
  return this.expandScenarios();
@@ -2487,10 +2870,11 @@ class LoadStrikeTrafficMix {
2487
2870
  }
2488
2871
  exports.LoadStrikeTrafficMix = LoadStrikeTrafficMix;
2489
2872
  class LoadStrikeRunner {
2490
- constructor(scenarios, options, contextConfigurators = []) {
2873
+ constructor(scenarios, options, contextConfigurators = [], internalOptions = {}) {
2491
2874
  this.scenarios = scenarios;
2492
2875
  this.options = normalizeRunnerOptionCollectionShapes(options);
2493
2876
  this.contextConfigurators = [...contextConfigurators];
2877
+ this.internalOptions = internalOptions;
2494
2878
  }
2495
2879
  /**
2496
2880
  * Creates a new instance of this public SDK type.
@@ -2526,7 +2910,7 @@ class LoadStrikeRunner {
2526
2910
  * Use this when one total load profile should be split across weighted scenario lanes.
2527
2911
  */
2528
2912
  static registerTrafficMix(trafficMix) {
2529
- return LoadStrikeRunner.registerScenarios(...expandTrafficMixScenarios(trafficMix));
2913
+ return LoadStrikeRunner.registerScenarios(...expandTrafficMixScenarios(trafficMix, 0));
2530
2914
  }
2531
2915
  /**
2532
2916
  * Registers a traffic mix on a fresh runnable context.
@@ -2542,6 +2926,12 @@ class LoadStrikeRunner {
2542
2926
  static DisplayConsoleMetrics(context, enable) {
2543
2927
  return context.DisplayConsoleMetrics(enable);
2544
2928
  }
2929
+ static UseLoadEngineV2(context) {
2930
+ return context.UseLoadEngineV2();
2931
+ }
2932
+ static WithMaxInFlight(context, maxInFlight) {
2933
+ return context.WithMaxInFlight(maxInFlight);
2934
+ }
2545
2935
  /**
2546
2936
  * Toggles local development cluster mode.
2547
2937
  * Use this when you want to simulate coordinator and agent behavior on a single machine.
@@ -2577,6 +2967,12 @@ class LoadStrikeRunner {
2577
2967
  static WithAgentGroup(context, agentGroup) {
2578
2968
  return context.WithAgentGroup(agentGroup);
2579
2969
  }
2970
+ static WithAgentId(context, agentId) {
2971
+ return context.WithAgentId(agentId);
2972
+ }
2973
+ static WithExpectedAgentIds(context, ...agentIds) {
2974
+ return context.WithExpectedAgentIds(...agentIds);
2975
+ }
2580
2976
  /**
2581
2977
  * Sets the requested agent count.
2582
2978
  * Use this when a coordinator should fan work out across a specific number of agents.
@@ -2794,7 +3190,10 @@ class LoadStrikeRunner {
2794
3190
  * Use this when one total load profile should be split across weighted scenario lanes.
2795
3191
  */
2796
3192
  addTrafficMix(trafficMix) {
2797
- this.scenarios = [...this.scenarios, ...expandTrafficMixScenarios(trafficMix)];
3193
+ this.scenarios = [
3194
+ ...this.scenarios,
3195
+ ...expandTrafficMixScenarios(trafficMix, nextTrafficMixDeclarationIndex(this.scenarios))
3196
+ ];
2798
3197
  return this;
2799
3198
  }
2800
3199
  /**
@@ -2927,6 +3326,19 @@ class LoadStrikeRunner {
2927
3326
  WithReportingInterval(intervalSeconds) {
2928
3327
  return this.withReportingInterval(intervalSeconds);
2929
3328
  }
3329
+ useLoadEngineV2() {
3330
+ return this.configure({ loadEngineContractVersion: 2 });
3331
+ }
3332
+ UseLoadEngineV2() {
3333
+ return this.useLoadEngineV2();
3334
+ }
3335
+ withMaxInFlight(maxInFlight) {
3336
+ validateV2MaxInFlight(this.options.loadEngineContractVersion, maxInFlight);
3337
+ return this.configure({ maxInFlight });
3338
+ }
3339
+ WithMaxInFlight(maxInFlight) {
3340
+ return this.withMaxInFlight(maxInFlight);
3341
+ }
2930
3342
  withReportingSinks(...sinks) {
2931
3343
  if (!sinks.length) {
2932
3344
  throw new Error("At least one reporting sink should be provided.");
@@ -3025,7 +3437,7 @@ class LoadStrikeRunner {
3025
3437
  }
3026
3438
  async run(args = []) {
3027
3439
  if (this.contextConfigurators.length) {
3028
- return new LoadStrikeRunner(this.scenarios, this.buildContext().toRunnerOptions()).run(args);
3440
+ return new LoadStrikeRunner(this.scenarios, this.buildContext().toRunnerOptions(), [], this.internalOptions).run(args);
3029
3441
  }
3030
3442
  if (args.length) {
3031
3443
  return this.buildContext().run(args);
@@ -3073,10 +3485,15 @@ class LoadStrikeRunner {
3073
3485
  let licenseClient = null;
3074
3486
  let licensePayload = null;
3075
3487
  let licenseSession = null;
3488
+ let iterationObservationsFinalized = false;
3076
3489
  const clusterMode = resolveClusterExecutionMode(this.options);
3077
3490
  const selectedScenarios = clusterMode === "local-coordinator" || clusterMode === "nats-coordinator"
3078
3491
  ? await this.filterScenariosWithPolicies(this.scenarios, policies, policyErrors, runtimePolicyErrorMode)
3079
3492
  : await this.selectScenarios(policies, policyErrors, runtimePolicyErrorMode);
3493
+ if (this.options.loadEngineContractVersion === 2
3494
+ && (clusterMode === "nats-coordinator" || clusterMode === "nats-agent")) {
3495
+ validateLoadEngineV2ScenarioFeatures(selectedScenarios);
3496
+ }
3080
3497
  if (clusterMode === "nats-agent") {
3081
3498
  return this.runAgentWithNats(createdUtc, testInfo, nodeInfo);
3082
3499
  }
@@ -3141,6 +3558,38 @@ class LoadStrikeRunner {
3141
3558
  }
3142
3559
  }
3143
3560
  await this.startSinks(sinkStates, sessionInfo, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
3561
+ const iterationObservationRunId = String(this.internalOptions.iterationObservationRunId
3562
+ ?? sessionInfo.portalReportingRunId
3563
+ ?? sessionInfo.PortalReportingRunId
3564
+ ?? testInfo.sessionId);
3565
+ const iterationObservationResultOwnerId = String(nodeInfo.nodeType).toLowerCase() === "agent"
3566
+ ? String(this.options.agentCommandId
3567
+ ?? `${nodeInfo.machineName}:${Math.max(Math.trunc(this.options.clusterShardIndex ?? 0), 0)}`)
3568
+ : "";
3569
+ const iterationObservationProcessGroup = 0;
3570
+ const iterationObservationExpectedResultOwnerCount64 = Math.max(Math.trunc(this.options.clusterShardCount ?? 1), 1).toString();
3571
+ const initializedIterationObservationSinks = sinkStates
3572
+ .filter((state) => !state.disabled)
3573
+ .map((state) => ({
3574
+ name: state.name,
3575
+ iterationObservationPortalSink: Boolean(state.sink.iterationObservationPortalSink),
3576
+ iterationObservationShapeLimited: Boolean(state.sink.iterationObservationShapeLimited),
3577
+ saveIterationBatch: resolveSinkSaveIterationBatch(state.sink),
3578
+ completeIterationObservationStream: resolveSinkCompleteIterationObservationStream(state.sink)
3579
+ }));
3580
+ const reporterIterationObservationSinks = this.internalOptions.iterationObservationSinks
3581
+ ?? (clusterMode === "local-coordinator" || clusterMode === "nats-coordinator"
3582
+ ? []
3583
+ : initializedIterationObservationSinks);
3584
+ const iterationObservationReporter = new iteration_observations_js_1.IterationObservationReporter({
3585
+ runId: iterationObservationRunId,
3586
+ sessionId: testInfo.sessionId,
3587
+ resultOwnerId: iterationObservationResultOwnerId,
3588
+ expectedResultOwnerCount64: iterationObservationExpectedResultOwnerCount64,
3589
+ processGroup: iterationObservationProcessGroup,
3590
+ settings: resolveIterationObservationSettings(this.options),
3591
+ sinks: reporterIterationObservationSinks
3592
+ });
3144
3593
  const emitRealtimeSnapshot = async () => {
3145
3594
  if (realtimeInFlight) {
3146
3595
  return;
@@ -3207,21 +3656,30 @@ class LoadStrikeRunner {
3207
3656
  let result;
3208
3657
  let metricStats;
3209
3658
  if (clusterMode === "local-coordinator") {
3210
- const aggregated = await this.runCoordinatorWithLocalAgents(selectedScenarios, testInfo, nodeInfo, licenseClient, licenseSession);
3659
+ const aggregated = await this.runCoordinatorWithLocalAgents(selectedScenarios, testInfo, nodeInfo, licenseClient, licenseSession, iterationObservationRunId, initializedIterationObservationSinks);
3211
3660
  metricStats = aggregated.metrics;
3212
3661
  result = toDetailedRunResultFromNodeStats(aggregated, started.toISOString(), sinkErrors, policyErrors);
3213
3662
  }
3214
3663
  else if (clusterMode === "nats-coordinator") {
3215
- const aggregated = await this.runCoordinatorWithNats(selectedScenarios, testInfo, nodeInfo, licenseClient, licenseSession);
3664
+ const aggregated = await this.runCoordinatorWithNats(selectedScenarios, testInfo, nodeInfo, licenseClient, licenseSession, iterationObservationRunId, initializedIterationObservationSinks);
3216
3665
  metricStats = aggregated.metrics;
3217
3666
  result = toDetailedRunResultFromNodeStats(aggregated, started.toISOString(), sinkErrors, policyErrors);
3218
3667
  }
3219
3668
  else {
3220
3669
  const testAbortController = new AbortController();
3221
3670
  const stopTestState = { value: false, reason: undefined };
3222
- await Promise.all(selectedScenarios.map((scenario, scenarioIndex) => executeScenarioRuntime({
3671
+ const loadEngineV2Budget = this.options.loadEngineContractVersion === 2
3672
+ ? (this.options.loadEngineV2BudgetOverride
3673
+ ?? new load_engine_v2_js_1.LoadEngineV2ExecutionBudget(this.options.maxInFlight ?? 10000))
3674
+ : undefined;
3675
+ const loadEngineV2Telemetry = loadEngineV2Budget
3676
+ ? new LoadEngineV2Telemetry(loadEngineV2Budget)
3677
+ : undefined;
3678
+ const executeSelectedScenario = (scenario, selectedScenarioIndex) => executeScenarioRuntime({
3223
3679
  scenario,
3224
- scenarioIndex,
3680
+ scenarioIndex: this.options.loadEngineContractVersion === 2
3681
+ ? this.scenarios.indexOf(scenario)
3682
+ : selectedScenarioIndex,
3225
3683
  scenarioCount: selectedScenarios.length,
3226
3684
  options: this.options,
3227
3685
  logger: runLogger,
@@ -3236,12 +3694,26 @@ class LoadStrikeRunner {
3236
3694
  scenarioDurationsMs,
3237
3695
  stopTestState,
3238
3696
  testAbortController,
3697
+ loadEngineV2Budget,
3698
+ loadEngineV2Telemetry,
3699
+ iterationObservationReporter,
3700
+ iterationObservationRunId,
3701
+ iterationObservationResultOwnerId,
3702
+ iterationObservationProcessGroup,
3239
3703
  executeScenarioInvocation: (targetScenario, context, operation) => this.executeScenarioInvocation(targetScenario, context, operation),
3240
3704
  invokeBeforeScenario: (runtimePolicies, scenarioName) => this.invokeBeforeScenario(runtimePolicies, scenarioName, policyErrors, runtimePolicyErrorMode),
3241
3705
  invokeAfterScenario: (runtimePolicies, scenarioName, stats) => this.invokeAfterScenario(runtimePolicies, scenarioName, stats, policyErrors, runtimePolicyErrorMode),
3242
3706
  invokeBeforeStep: (runtimePolicies, scenarioName, stepName) => this.invokeBeforeStep(runtimePolicies, scenarioName, stepName, policyErrors, runtimePolicyErrorMode),
3243
3707
  invokeAfterStep: (runtimePolicies, scenarioName, stepName, reply) => this.invokeAfterStep(runtimePolicies, scenarioName, stepName, reply, policyErrors, runtimePolicyErrorMode)
3244
- })));
3708
+ });
3709
+ if (this.options.loadEngineV2SegmentLifecycleOverride) {
3710
+ for (let scenarioIndex = 0; scenarioIndex < selectedScenarios.length; scenarioIndex += 1) {
3711
+ await executeSelectedScenario(selectedScenarios[scenarioIndex], scenarioIndex);
3712
+ }
3713
+ }
3714
+ else {
3715
+ await Promise.all(selectedScenarios.map(executeSelectedScenario));
3716
+ }
3245
3717
  nodeInfo.currentOperation = stopTestState.value ? "Stop" : "Complete";
3246
3718
  const scenarioStatList = Array.from(scenarioAccumulators.values())
3247
3719
  .map((value) => value.build(scenarioDurationsMs.get(value.scenarioName) ?? 0))
@@ -3281,10 +3753,39 @@ class LoadStrikeRunner {
3281
3753
  reportFiles: [],
3282
3754
  logFiles: [...loggerSetup.logFiles],
3283
3755
  correlationRows: buildDetailedCorrelationRows(),
3284
- failedCorrelationRows: buildDetailedFailedCorrelationRows()
3756
+ failedCorrelationRows: buildDetailedFailedCorrelationRows(),
3757
+ ...(loadEngineV2Telemetry
3758
+ ? {
3759
+ generatorWarnings: loadEngineV2Telemetry.buildWarnings(),
3760
+ schedulerSegments: loadEngineV2Telemetry.buildSegments(),
3761
+ schedulerStats: loadEngineV2Telemetry.buildStats(),
3762
+ [LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS]: loadEngineV2Telemetry.buildSchedulerDistributions()
3763
+ }
3764
+ : {})
3285
3765
  };
3286
3766
  }
3287
3767
  await stopRealtimeReporting();
3768
+ const observationDelivery = await iterationObservationReporter.sealAndDrain();
3769
+ iterationObservationsFinalized = true;
3770
+ if (clusterMode !== "local-coordinator"
3771
+ && clusterMode !== "nats-coordinator") {
3772
+ result.observationDeliveryStats = {
3773
+ lastBatchSequence64: observationDelivery.lastBatchSequence64,
3774
+ capturedCount64: observationDelivery.capturedCount64,
3775
+ deliveredCount64: observationDelivery.deliveredCount64,
3776
+ droppedBufferCount64: observationDelivery.droppedBufferCount64,
3777
+ droppedSinkCount64: observationDelivery.droppedSinkCount64
3778
+ };
3779
+ result.reportingComplete = observationDelivery.reportingComplete;
3780
+ }
3781
+ else {
3782
+ result.observationDeliveryStats ?? (result.observationDeliveryStats = emptyObservationDeliveryStats());
3783
+ result.reportingComplete ?? (result.reportingComplete = observationDelivery.reportingComplete);
3784
+ }
3785
+ result.generatorWarnings = [
3786
+ ...(result.generatorWarnings ?? []),
3787
+ ...iterationObservationReporter.buildWarnings()
3788
+ ];
3288
3789
  result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
3289
3790
  const finalizedResult = attachRunResultAliases(result);
3290
3791
  finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
@@ -3300,6 +3801,9 @@ class LoadStrikeRunner {
3300
3801
  }
3301
3802
  finally {
3302
3803
  await stopRealtimeReporting();
3804
+ if (!iterationObservationsFinalized) {
3805
+ await iterationObservationReporter.sealAndDrain().catch(() => { });
3806
+ }
3303
3807
  if (!sinksStopped) {
3304
3808
  await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
3305
3809
  }
@@ -3336,7 +3840,7 @@ class LoadStrikeRunner {
3336
3840
  }
3337
3841
  return filtered;
3338
3842
  }
3339
- async runClusterChildNode(targetScenarios, nodeType, machineName, includeWorkerExtensions, overrides = {}) {
3843
+ async runClusterChildNode(targetScenarios, nodeType, machineName, includeWorkerExtensions, overrides = {}, internalOptions = {}) {
3340
3844
  if (!targetScenarios.length) {
3341
3845
  return buildEmptyNodeStats({
3342
3846
  startedUtc: new Date().toISOString(),
@@ -3364,7 +3868,7 @@ class LoadStrikeRunner {
3364
3868
  displayConsoleMetrics: false,
3365
3869
  reportingSinks: includeWorkerExtensions ? this.options.reportingSinks : [],
3366
3870
  workerPlugins: includeWorkerExtensions ? this.options.workerPlugins : []
3367
- });
3871
+ }, [], internalOptions);
3368
3872
  const childResult = await childRunner.run();
3369
3873
  const childStats = detailedToNodeStats(childResult);
3370
3874
  return {
@@ -3377,70 +3881,127 @@ class LoadStrikeRunner {
3377
3881
  logFiles: [...(childResult.logFiles ?? [])]
3378
3882
  };
3379
3883
  }
3380
- async runCoordinatorWithLocalAgents(scenarios, testInfo, nodeInfo, licenseClient, licenseSession) {
3884
+ async runCoordinatorWithLocalAgents(scenarios, testInfo, nodeInfo, licenseClient, licenseSession, iterationObservationRunId, iterationObservationSinks) {
3381
3885
  if (!licenseClient) {
3382
3886
  throw new Error("Coordinator agent execution authorization requires an initialized licensing client.");
3383
3887
  }
3384
- const controllerRunToken = stringValueOrDefault(licenseSession?.runToken, "").trim();
3888
+ const controllerRunToken = currentLicenseSessionRunToken(licenseSession);
3385
3889
  if (!controllerRunToken) {
3386
3890
  throw new Error("Coordinator agent execution authorization requires an active controller run token.");
3387
3891
  }
3388
- const assignments = planRuntimeClusterAssignments(scenarios, Math.max(this.options.agentsCount ?? 1, 1), this.options.targetScenarios ?? [], this.options.agentTargetScenarios ?? []);
3892
+ const assignments = planRuntimeClusterAssignments(scenarios, Math.max(this.options.agentsCount ?? 1, 1), this.options.targetScenarios ?? [], this.options.agentTargetScenarios ?? [], this.options.loadEngineContractVersion === 2, this.options.coordinatorTargetScenarios ?? []);
3893
+ const sharedV2Budget = this.options.loadEngineContractVersion === 2
3894
+ ? new load_engine_v2_js_1.LoadEngineV2ExecutionBudget(this.options.maxInFlight ?? 10000)
3895
+ : undefined;
3389
3896
  const nodeResults = await Promise.all(assignments.map(async (targetScenarios, index) => {
3390
3897
  const commandId = (0, node_crypto_1.randomBytes)(16).toString("hex");
3391
- const agentExecutionToken = await licenseClient.createAgentExecutionToken(controllerRunToken, testInfo.sessionId, commandId, index, assignments.length, targetScenarios);
3898
+ const agentExecutionToken = await licenseClient.createAgentExecutionToken(currentLicenseSessionRunToken(licenseSession), testInfo.sessionId, commandId, index, assignments.length, targetScenarios);
3392
3899
  return this.runClusterChildNode(targetScenarios, "Agent", `local-agent-${index + 1}`, false, {
3393
3900
  sessionId: testInfo.sessionId,
3394
3901
  testSuite: testInfo.testSuite,
3395
3902
  testName: testInfo.testName,
3396
3903
  agentCommandId: commandId,
3397
- agentExecutionToken
3904
+ agentExecutionToken,
3905
+ clusterShardIndex: index,
3906
+ clusterShardCount: assignments.length,
3907
+ loadEngineV2BudgetOverride: sharedV2Budget
3908
+ }, {
3909
+ iterationObservationRunId,
3910
+ iterationObservationSinks
3398
3911
  });
3399
3912
  }));
3400
3913
  const coordinatorTargets = [...(this.options.coordinatorTargetScenarios ?? [])];
3401
3914
  if (coordinatorTargets.length) {
3402
- nodeResults.push(await this.runClusterChildNode(coordinatorTargets, "Coordinator", nodeInfo.machineName, false));
3915
+ nodeResults.push(await this.runClusterChildNode(coordinatorTargets, "Coordinator", nodeInfo.machineName, false, {}, {
3916
+ iterationObservationRunId,
3917
+ iterationObservationSinks
3918
+ }));
3403
3919
  }
3404
- return aggregateNodeStats(testInfo, { ...nodeInfo, nodeType: "Coordinator" }, nodeResults);
3920
+ return aggregateNodeStats(testInfo, { ...nodeInfo, nodeType: "Coordinator" }, nodeResults, this.options.loadEngineContractVersion === 2);
3405
3921
  }
3406
- async runCoordinatorWithNats(scenarios, testInfo, nodeInfo, licenseClient, licenseSession) {
3922
+ async runCoordinatorWithNats(scenarios, testInfo, nodeInfo, licenseClient, licenseSession, iterationObservationRunId, iterationObservationSinks) {
3407
3923
  if (!licenseClient) {
3408
3924
  throw new Error("Coordinator agent execution authorization requires an initialized licensing client.");
3409
3925
  }
3410
- const controllerRunToken = stringValueOrDefault(licenseSession?.runToken, "").trim();
3926
+ const controllerRunToken = currentLicenseSessionRunToken(licenseSession);
3411
3927
  if (!controllerRunToken) {
3412
3928
  throw new Error("Coordinator agent execution authorization requires an active controller run token.");
3413
3929
  }
3414
- const assignments = planRuntimeClusterAssignments(scenarios, Math.max(this.options.agentsCount ?? 1, 1), this.options.targetScenarios ?? [], this.options.agentTargetScenarios ?? []);
3930
+ const assignments = planRuntimeClusterAssignments(scenarios, Math.max(this.options.agentsCount ?? 1, 1), this.options.targetScenarios ?? [], this.options.agentTargetScenarios ?? [], this.options.loadEngineContractVersion === 2, this.options.coordinatorTargetScenarios ?? []);
3931
+ const expectedAgentIds = this.options.loadEngineContractVersion === 2
3932
+ ? normalizeRequiredV2AgentIds(this.options.expectedAgentIds, assignments.length)
3933
+ : undefined;
3415
3934
  const coordinator = new cluster_js_1.DistributedClusterCoordinator({
3416
3935
  clusterId: this.options.clusterId ?? "local",
3417
3936
  sessionId: testInfo.sessionId,
3418
3937
  testSuite: testInfo.testSuite,
3419
3938
  testName: testInfo.testName,
3420
3939
  expectedAgentResults: assignments.length,
3940
+ expectedAgentIds,
3941
+ loadEngineContractVersion: this.options.loadEngineContractVersion ?? 1,
3421
3942
  agentGroup: this.options.agentGroup,
3422
3943
  commandTimeoutMs: Math.max(Math.trunc((this.options.clusterCommandTimeoutSeconds ?? 120) * 1000), 1),
3423
3944
  nats: this.options.natsServerUrl
3424
3945
  ? { ServerUrl: this.options.natsServerUrl }
3425
3946
  : undefined
3426
3947
  });
3427
- const dispatch = await coordinator.dispatch(assignments, (command) => licenseClient.createAgentExecutionToken(controllerRunToken, testInfo.sessionId, command.commandId, command.agentIndex, command.agentCount, command.targetScenarios));
3428
- const nodes = dispatch.nodeResults.map((value) => clusterNodeResultToNodeStats(value, testInfo, { ...nodeInfo, nodeType: "Agent" }));
3948
+ const tokenFactory = (command) => licenseClient.createAgentExecutionToken(currentLicenseSessionRunToken(licenseSession), testInfo.sessionId, command.commandId, command.agentIndex, command.agentCount, command.targetScenarios);
3949
+ const dispatch = this.options.loadEngineContractVersion === 2
3950
+ ? await coordinator.dispatchV2(assignments, buildRuntimeLoadEngineV2Plan(scenarios, this.options, testInfo, expectedAgentIds), tokenFactory)
3951
+ : await coordinator.dispatch(assignments, tokenFactory);
3952
+ const nodes = dispatch.nodeResults.map((value) => clusterNodeResultToNodeStats(value, testInfo, { ...nodeInfo, nodeType: "Agent" }, this.options.loadEngineContractVersion === 2));
3429
3953
  const coordinatorTargets = [...(this.options.coordinatorTargetScenarios ?? [])];
3430
3954
  if (coordinatorTargets.length) {
3431
- nodes.push(await this.runClusterChildNode(coordinatorTargets, "Coordinator", nodeInfo.machineName, false));
3955
+ nodes.push(await this.runClusterChildNode(coordinatorTargets, "Coordinator", nodeInfo.machineName, false, {}, {
3956
+ iterationObservationRunId,
3957
+ iterationObservationSinks
3958
+ }));
3432
3959
  }
3433
- let aggregated = aggregateNodeStats(testInfo, { ...nodeInfo, nodeType: "Coordinator" }, nodes);
3960
+ let aggregated = aggregateNodeStats(testInfo, { ...nodeInfo, nodeType: "Coordinator" }, nodes, this.options.loadEngineContractVersion === 2);
3434
3961
  if (dispatch.missingNodes > 0) {
3435
3962
  aggregated = appendClusterPluginHint(aggregated, `Timed out waiting for ${dispatch.missingNodes} agent node result(s).`);
3963
+ aggregated = attachNodeStatsAliases({
3964
+ ...aggregated,
3965
+ reportingComplete: false,
3966
+ schedulerSegments: [
3967
+ ...(aggregated.schedulerSegments ?? []),
3968
+ ...(dispatch.ownerLoss?.schedulerSegments ?? [])
3969
+ ],
3970
+ generatorWarnings: [
3971
+ ...aggregated.generatorWarnings,
3972
+ ...(dispatch.ownerLoss?.generatorWarnings ?? []).map((warning) => ({
3973
+ code: warning.code,
3974
+ scenarioName: "cluster",
3975
+ simulationIndex: -1,
3976
+ count64: warning.count64,
3977
+ message: `Result owner ${warning.agentId} was lost after assignment; application failures were not fabricated.`,
3978
+ firstObservedUtcNs: "0",
3979
+ lastObservedUtcNs: "0"
3980
+ }))
3981
+ ]
3982
+ });
3436
3983
  }
3437
3984
  return aggregated;
3438
3985
  }
3439
3986
  async runAgentWithNats(startedUtc, testInfo, nodeInfo) {
3987
+ const v2AgentId = this.options.loadEngineContractVersion === 2
3988
+ ? requireNonEmpty(this.options.agentId ?? "", "Remote Load Engine V2 agents require an explicit stable AgentId.")
3989
+ : `${nodeInfo.machineName}-${generateRuntimeSessionId()}`;
3440
3990
  const agent = new cluster_js_1.DistributedClusterAgent({
3441
3991
  clusterId: this.options.clusterId ?? "local",
3442
3992
  sessionId: testInfo.sessionId,
3443
- agentId: `${nodeInfo.machineName}-${generateRuntimeSessionId()}`,
3993
+ agentId: v2AgentId,
3994
+ loadEngineContractVersion: this.options.loadEngineContractVersion ?? 1,
3995
+ validateV2Plan: this.options.loadEngineContractVersion === 2
3996
+ ? (received) => {
3997
+ const local = buildRuntimeLoadEngineV2Plan(this.scenarios, this.options, { ...testInfo, sessionId: received.sessionId }, received.expectedAgentIds);
3998
+ local.runId = received.runId;
3999
+ local.registrationNonce = received.registrationNonce;
4000
+ if ((0, cluster_js_1.buildLoadEngineV2Plan)(local).hash !== (0, cluster_js_1.buildLoadEngineV2Plan)(received).hash) {
4001
+ throw new Error("Load Engine V2 signed plan descriptors do not match immutable local scenario declarations.");
4002
+ }
4003
+ }
4004
+ : undefined,
3444
4005
  agentGroup: this.options.agentGroup,
3445
4006
  nats: this.options.natsServerUrl
3446
4007
  ? { ServerUrl: this.options.natsServerUrl }
@@ -3450,13 +4011,16 @@ class LoadStrikeRunner {
3450
4011
  const deadline = Date.now() + Math.max(Math.trunc((this.options.clusterCommandTimeoutSeconds ?? 120) * 1000), 1);
3451
4012
  while (Date.now() < deadline) {
3452
4013
  let handledStats = null;
3453
- const handled = await agent.pollAndExecuteOnce(async (dispatch) => {
4014
+ const execute = async (dispatch) => {
3454
4015
  handledStats = await this.runClusterChildNode(dispatch.scenarioNames, "Agent", nodeInfo.machineName, true, {
3455
4016
  sessionId: testInfo.sessionId,
3456
4017
  testSuite: testInfo.testSuite,
3457
4018
  testName: testInfo.testName,
3458
4019
  agentCommandId: dispatch.commandId,
3459
- agentExecutionToken: dispatch.agentRunToken
4020
+ agentExecutionToken: dispatch.agentRunToken,
4021
+ clusterShardIndex: dispatch.agentIndex ?? 0,
4022
+ clusterShardCount: dispatch.agentCount ?? 1,
4023
+ loadEngineV2SegmentLifecycleOverride: dispatch.segmentLifecycle
3460
4024
  });
3461
4025
  return {
3462
4026
  nodeId: handledStats.nodeInfo.machineName,
@@ -3464,9 +4028,12 @@ class LoadStrikeRunner {
3464
4028
  allRequestCount: handledStats.allRequestCount,
3465
4029
  allOkCount: handledStats.allOkCount,
3466
4030
  allFailCount: handledStats.allFailCount,
3467
- stats: nodeStatsToClusterPayload(handledStats)
4031
+ stats: nodeStatsToClusterPayload(handledStats, this.scenarios.filter((scenario) => dispatch.scenarioNames.includes(scenario.name)), this.options.loadEngineContractVersion === 2)
3468
4032
  };
3469
- });
4033
+ };
4034
+ const handled = this.options.loadEngineContractVersion === 2
4035
+ ? await agent.pollAndExecuteV2Once(execute)
4036
+ : await agent.pollAndExecuteOnce(execute);
3470
4037
  if (handled && handledStats) {
3471
4038
  return toDetailedRunResultFromNodeStats(handledStats, startedUtc, [], []);
3472
4039
  }
@@ -3746,13 +4313,148 @@ function hasPluginRows(value) {
3746
4313
  }
3747
4314
  return value.tables.some((table) => Array.isArray(table.rows) && table.rows.length > 0);
3748
4315
  }
4316
+ async function executeV2FixedArrivals(args) {
4317
+ const { rate, intervalNs, totalArrivals, budget, cancellationToken, shouldStopNow, nextInstanceInfo, runBombingInvocation, logger, scenarioName, deadlineOffsetsNs, tolerancesNs, ownedOrdinals, shardIndex = 0, shardCount = 1, telemetry, segment } = args;
4318
+ const segmentStartNs = process.hrtime.bigint();
4319
+ const toleranceNs = (0, load_engine_v2_js_1.loadEngineV2LatenessToleranceNs)(rate, intervalNs);
4320
+ const active = new Set();
4321
+ let schedulerLate = 0n;
4322
+ let maxInFlight = 0n;
4323
+ let executionError;
4324
+ const normalizedShardCount = Math.max(Math.trunc(shardCount), 1);
4325
+ const normalizedShardIndex = Math.min(Math.max(Math.trunc(shardIndex), 0), normalizedShardCount - 1);
4326
+ const ordinals = ownedOrdinals
4327
+ ? [...ownedOrdinals]
4328
+ : (() => {
4329
+ const values = [];
4330
+ for (let ordinal = BigInt(normalizedShardIndex); ordinal < totalArrivals; ordinal += BigInt(normalizedShardCount)) {
4331
+ values.push(ordinal);
4332
+ }
4333
+ return values;
4334
+ })();
4335
+ if (ordinals.some((ordinal, index) => ordinal < 0n || ordinal >= totalArrivals
4336
+ || (index > 0 && ordinal <= ordinals[index - 1]))) {
4337
+ throw new Error("Load Engine V2 owned arrival ordinals must be sorted, unique, and in range.");
4338
+ }
4339
+ if (segment) {
4340
+ segment.planned = BigInt(ordinals.length);
4341
+ }
4342
+ for (const ordinal of ordinals) {
4343
+ if (shouldStopNow())
4344
+ break;
4345
+ const index = Number(ordinal);
4346
+ const deadlineNs = segmentStartNs
4347
+ + (deadlineOffsetsNs?.[index] ?? (0, load_engine_v2_js_1.loadEngineV2FixedDeadlineNs)(ordinal, rate, intervalNs));
4348
+ await delayUntilMonotonicDeadline(deadlineNs, cancellationToken);
4349
+ if (shouldStopNow()) {
4350
+ break;
4351
+ }
4352
+ const nowNs = process.hrtime.bigint();
4353
+ if (segment)
4354
+ segment.due += 1n;
4355
+ if (segment)
4356
+ telemetry?.recordDecisionLag(segment, nowNs - deadlineNs);
4357
+ if ((0, load_engine_v2_js_1.classifyLoadEngineV2Arrival)(nowNs, deadlineNs, tolerancesNs?.[index] ?? toleranceNs, true) === "scheduler_late") {
4358
+ schedulerLate += 1n;
4359
+ if (segment) {
4360
+ segment.dropped += 1n;
4361
+ incrementReason(segment.dropReasons, "scheduler_late");
4362
+ }
4363
+ continue;
4364
+ }
4365
+ const release = budget.tryAcquire();
4366
+ if (!release) {
4367
+ maxInFlight += 1n;
4368
+ if (segment) {
4369
+ segment.dropped += 1n;
4370
+ incrementReason(segment.dropReasons, "max_in_flight");
4371
+ }
4372
+ continue;
4373
+ }
4374
+ if (segment)
4375
+ segment.started += 1n;
4376
+ const instanceInfo = nextInstanceInfo();
4377
+ let task;
4378
+ task = (async () => {
4379
+ if (segment)
4380
+ telemetry?.recordStartLag(segment, process.hrtime.bigint() - deadlineNs);
4381
+ await runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId, ordinal);
4382
+ })().catch((error) => {
4383
+ executionError ?? (executionError = error);
4384
+ }).finally(() => {
4385
+ if (segment)
4386
+ segment.completed += 1n;
4387
+ release();
4388
+ active.delete(task);
4389
+ });
4390
+ active.add(task);
4391
+ }
4392
+ while (active.size > 0) {
4393
+ await Promise.race(active);
4394
+ }
4395
+ if (segment) {
4396
+ segment.unreached = segment.planned > segment.due ? segment.planned - segment.due : 0n;
4397
+ segment.accountingComplete = segment.due + segment.unreached === segment.planned
4398
+ && segment.started + segment.dropped === segment.due
4399
+ && segment.completed === segment.started;
4400
+ }
4401
+ if (telemetry && segment) {
4402
+ telemetry.recordWarning("scheduler_late", segment, schedulerLate);
4403
+ telemetry.recordWarning("max_in_flight", segment, maxInFlight);
4404
+ }
4405
+ if (schedulerLate > 0n) {
4406
+ logger.warn(`Load Engine V2 warning scheduler_late: dropped ${schedulerLate.toString()} overdue arrivals for scenario ${scenarioName}.`);
4407
+ }
4408
+ if (maxInFlight > 0n) {
4409
+ logger.warn(`Load Engine V2 warning max_in_flight: dropped ${maxInFlight.toString()} arrivals for scenario ${scenarioName}; the process limit is ${budget.maxInFlight}.`);
4410
+ }
4411
+ if (executionError !== undefined) {
4412
+ throw executionError;
4413
+ }
4414
+ }
4415
+ async function delayUntilMonotonicDeadline(deadlineNs, cancellationToken) {
4416
+ while (!cancellationToken.aborted) {
4417
+ const remainingNs = deadlineNs - process.hrtime.bigint();
4418
+ if (remainingNs <= 0n) {
4419
+ return;
4420
+ }
4421
+ const remainingMs = Number((remainingNs + 999999n) / 1000000n);
4422
+ await delayWithAbort(Math.max(1, Math.min(remainingMs, 50)), cancellationToken);
4423
+ }
4424
+ }
4425
+ function secondsToNanoseconds(seconds, name) {
4426
+ if (!Number.isFinite(seconds) || seconds <= 0) {
4427
+ throw new RangeError(`${name} must be greater than zero.`);
4428
+ }
4429
+ const nanoseconds = Math.trunc(seconds * 1000000000);
4430
+ if (!Number.isSafeInteger(nanoseconds) || nanoseconds <= 0) {
4431
+ throw new RangeError(`${name} is outside the supported nanosecond range.`);
4432
+ }
4433
+ return BigInt(nanoseconds);
4434
+ }
4435
+ function buildV2RampTolerances(offsets, durationNs) {
4436
+ return offsets.map((offset, index) => {
4437
+ const quantum = offsets.length === 1
4438
+ ? durationNs
4439
+ : index === 0
4440
+ ? offsets[1] - offset
4441
+ : offset - offsets[index - 1];
4442
+ return minBigInt(100000000n, maxBigInt(2000000n, maxBigInt(1n, quantum) * 4n));
4443
+ });
4444
+ }
4445
+ function minBigInt(left, right) {
4446
+ return left < right ? left : right;
4447
+ }
4448
+ function maxBigInt(left, right) {
4449
+ return left > right ? left : right;
4450
+ }
3749
4451
  async function executeScenarioRuntime(args) {
3750
- const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
4452
+ const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, loadEngineV2Budget, loadEngineV2Telemetry, iterationObservationReporter, iterationObservationRunId = testInfo.sessionId, iterationObservationResultOwnerId = "", iterationObservationProcessGroup = 0, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
3751
4453
  const scenarioStartedMs = Date.now();
3752
4454
  const scenarioContextData = {};
3753
4455
  const registeredMetrics = [];
3754
4456
  const runtime = ensureScenarioRuntime(scenarioRuntimes, scenario.name);
3755
- const accumulator = new ScenarioStatsAccumulator(scenario.name, scenarioIndex);
4457
+ const accumulator = new ScenarioStatsAccumulator(scenario.name, scenarioIndex, options.loadEngineContractVersion === 2);
3756
4458
  scenarioAccumulators.set(scenario.name, accumulator);
3757
4459
  const scenarioAbortController = new AbortController();
3758
4460
  const scenarioCancellationToken = combineAbortSignals(testAbortController.signal, scenarioAbortController.signal);
@@ -3761,12 +4463,25 @@ async function executeScenarioRuntime(args) {
3761
4463
  ? Date.now() + Math.trunc(scenarioCompletionTimeoutSeconds * 1000)
3762
4464
  : Number.POSITIVE_INFINITY;
3763
4465
  const scenarioPartition = attachScenarioPartitionAliases({
3764
- number: 0,
3765
- count: 1
4466
+ number: options.loadEngineContractVersion === 2 ? Math.max(Math.trunc(options.clusterShardIndex ?? 0), 0) : 0,
4467
+ count: options.loadEngineContractVersion === 2 ? Math.max(Math.trunc(options.clusterShardCount ?? 1), 1) : 1
3766
4468
  });
4469
+ const trafficMixV2 = options.loadEngineContractVersion === 2
4470
+ ? scenario.__loadStrikeTrafficMixV2Metadata()
4471
+ : undefined;
4472
+ if (trafficMixV2) {
4473
+ const expectedSeedId = (0, load_engine_v2_js_1.buildLoadEngineV2TrafficMixSeedId)(trafficMixV2.declarationIndex, trafficMixV2.name);
4474
+ if (trafficMixV2.seedId !== expectedSeedId) {
4475
+ throw new Error(`Load Engine V2 traffic-mix seed metadata differs for scenario ${scenario.name}.`);
4476
+ }
4477
+ }
3767
4478
  let stopScenario = false;
3768
4479
  let invocationNumber = 0;
3769
4480
  let instanceCounter = 0;
4481
+ let nextV1ObservationOrdinal = 0n;
4482
+ let nextV2FallbackOrdinal = BigInt(scenarioPartition.number);
4483
+ let nextObservationStepSortIndex = 0;
4484
+ const observationStepSortIndexes = new Map();
3770
4485
  const shouldStopNow = () => stopTestState.value
3771
4486
  || stopScenario
3772
4487
  || scenarioCancellationToken.aborted
@@ -3800,7 +4515,7 @@ async function executeScenarioRuntime(args) {
3800
4515
  runtime.maxLatencyMs = Math.max(runtime.maxLatencyMs, latencyMs);
3801
4516
  accumulator.recordScenario(reply, observedLatencyMs);
3802
4517
  };
3803
- const recordStepReply = (stepName, reply, observedLatencyMs) => {
4518
+ const recordStepReply = (stepName, reply, observedLatencyMs, sortIndex) => {
3804
4519
  const key = `${scenario.name}::${stepName}`;
3805
4520
  const stepRuntime = ensureStepRuntime(stepRuntimes, key, scenario.name, stepName);
3806
4521
  if (reply.isSuccess) {
@@ -3823,11 +4538,35 @@ async function executeScenarioRuntime(args) {
3823
4538
  stepRuntime.maxLatencyMs = Math.max(stepRuntime.maxLatencyMs, latencyMs);
3824
4539
  const statusCode = normalizeStatusCode(reply.statusCode, reply.isSuccess);
3825
4540
  stepRuntime.statusCodes[statusCode] = (stepRuntime.statusCodes[statusCode] ?? 0) + 1;
3826
- accumulator.recordStep(stepName, reply, observedLatencyMs);
4541
+ return accumulator.recordStep(stepName, reply, observedLatencyMs, sortIndex);
4542
+ };
4543
+ const resolveObservationStepSortIndex = (stepName) => {
4544
+ const existing = observationStepSortIndexes.get(stepName);
4545
+ if (existing !== undefined) {
4546
+ return existing;
4547
+ }
4548
+ nextObservationStepSortIndex += 1;
4549
+ observationStepSortIndexes.set(stepName, nextObservationStepSortIndex);
4550
+ return nextObservationStepSortIndex;
4551
+ };
4552
+ const nextObservationOrdinal = (explicit) => {
4553
+ if (explicit !== undefined) {
4554
+ return explicit;
4555
+ }
4556
+ if (options.loadEngineContractVersion === 2) {
4557
+ const ordinal = nextV2FallbackOrdinal;
4558
+ nextV2FallbackOrdinal += BigInt(scenarioPartition.count);
4559
+ return ordinal;
4560
+ }
4561
+ const ordinal = nextV1ObservationOrdinal;
4562
+ nextV1ObservationOrdinal += 1n;
4563
+ return ordinal;
3827
4564
  };
3828
4565
  const runSingleInvocation = async (operation, instanceData, instanceNumber, instanceId, recordScenarioResult) => {
3829
4566
  invocationNumber += 1;
3830
4567
  const runtimeRandom = createRuntimeRandom();
4568
+ const attemptSteps = [];
4569
+ const recordedSteps = [];
3831
4570
  const context = {
3832
4571
  scenarioName: scenario.name,
3833
4572
  data: scenarioContextData,
@@ -3856,7 +4595,12 @@ async function executeScenarioRuntime(args) {
3856
4595
  testAbortController.abort(stopTestState.reason);
3857
4596
  },
3858
4597
  recordStep: (stepName, reply, observedLatencyMs) => {
3859
- recordStepReply(stepName, reply, observedLatencyMs);
4598
+ const sortIndex = resolveObservationStepSortIndex(stepName);
4599
+ recordedSteps.push({ stepName, reply, observedLatencyMs, sortIndex });
4600
+ return sortIndex;
4601
+ },
4602
+ recordStepObservation: (observation) => {
4603
+ attemptSteps.push(observation);
3860
4604
  },
3861
4605
  shouldStopScenario: () => stopScenario || scenarioCancellationToken.aborted,
3862
4606
  shouldStopTest: () => stopTestState.value || scenarioCancellationToken.aborted,
@@ -3864,28 +4608,92 @@ async function executeScenarioRuntime(args) {
3864
4608
  invokeAfterStep: async (stepName, reply) => invokeAfterStep(policies, scenario.name, stepName, reply)
3865
4609
  };
3866
4610
  attachScenarioContextAliases(context);
3867
- const startedAt = Date.now();
4611
+ const startedUtcNs = (0, iteration_observations_js_1.utcNowNs)();
4612
+ const startedAtNs = process.hrtime.bigint();
3868
4613
  const reply = await executeScenarioInvocation(scenario, context, operation);
3869
- const observedLatencyMs = Math.max(Date.now() - startedAt, 0);
4614
+ const observedLatencyNs = maxBigInt(process.hrtime.bigint() - startedAtNs, 0n);
4615
+ const completedUtcNs = startedUtcNs + observedLatencyNs;
4616
+ const observedLatencyMs = Number(observedLatencyNs) / 1000000;
3870
4617
  if (recordScenarioResult) {
3871
4618
  recordScenarioReply(reply, observedLatencyMs);
3872
4619
  }
3873
- return { reply, observedLatencyMs };
4620
+ return {
4621
+ reply,
4622
+ observedLatencyMs,
4623
+ startedUtcNs,
4624
+ completedUtcNs,
4625
+ observedLatencyUs: observedLatencyNs / 1000n,
4626
+ reportedLatencyUs: normalizeRawObservationLatencyMicroseconds(resolveRecordedLatency(reply.customLatencyMs, observedLatencyMs)),
4627
+ steps: attemptSteps,
4628
+ recordedSteps
4629
+ };
4630
+ };
4631
+ const captureAttemptObservation = (operation, globalOrdinal, attemptIndex, isFinalAttempt, attempt, simulationIndex, simulationKind, iterationId, globalSecondaryOrdinal = 0n) => {
4632
+ if (!iterationObservationReporter?.enabled) {
4633
+ return;
4634
+ }
4635
+ iterationObservationReporter.capture((0, iteration_observations_js_1.createIterationObservation)({
4636
+ runId: iterationObservationRunId,
4637
+ sessionId: testInfo.sessionId,
4638
+ resultOwnerId: iterationObservationResultOwnerId,
4639
+ processGroup: iterationObservationProcessGroup,
4640
+ scenarioName: scenario.name,
4641
+ scenarioIndex,
4642
+ simulationIndex,
4643
+ simulationKind,
4644
+ phase: operation === "WarmUp" ? "warmup" : "bombing",
4645
+ globalOrdinal64: globalOrdinal,
4646
+ globalSecondaryOrdinal64: globalSecondaryOrdinal,
4647
+ ...(iterationId ? { iterationId } : {}),
4648
+ shardIndex: scenarioPartition.number,
4649
+ shardCount: scenarioPartition.count,
4650
+ attemptIndex,
4651
+ isFinalAttempt,
4652
+ startedUtcNs: attempt.startedUtcNs,
4653
+ completedUtcNs: attempt.completedUtcNs,
4654
+ observedLatencyUs64: attempt.observedLatencyUs,
4655
+ reportedLatencyUs64: attempt.reportedLatencyUs,
4656
+ isSuccess: attempt.reply.isSuccess,
4657
+ statusCode: normalizeStatusCode(attempt.reply.statusCode, attempt.reply.isSuccess),
4658
+ sizeBytes64: normalizeHistogramInteger(Math.max(toNumber(attempt.reply.sizeBytes), 0), "Scenario response bytes"),
4659
+ steps: attempt.steps
4660
+ }));
3874
4661
  };
3875
- const runBombingInvocation = async (instanceData, instanceNumber, instanceId) => {
4662
+ const runBombingInvocation = async (instanceData, instanceNumber, instanceId, explicitGlobalOrdinal, simulationIndex = -1, simulationKind = "SingleInvocation", explicitSecondaryOrdinal = 0n) => {
3876
4663
  if (shouldStopNow()) {
3877
4664
  return;
3878
4665
  }
4666
+ const globalOrdinal = nextObservationOrdinal(explicitGlobalOrdinal);
4667
+ const identityKind = canonicalLoadEngineV2InvocationIdentityKind(simulationKind);
4668
+ const iterationId = options.loadEngineContractVersion === 2
4669
+ && explicitGlobalOrdinal !== undefined
4670
+ && identityKind
4671
+ ? (0, cluster_js_1.buildLoadEngineV2GlobalInvocationId)(iterationObservationRunId, scenarioIndex, simulationIndex, identityKind, globalOrdinal, explicitSecondaryOrdinal)
4672
+ : undefined;
3879
4673
  let attempts = 0;
3880
4674
  const maxAttempts = 1 + (scenario.shouldRestartIterationOnFail() ? restartIterationMaxAttempts : 0);
3881
4675
  while (attempts < maxAttempts && !shouldStopNow()) {
3882
4676
  attempts += 1;
3883
- const attempt = await runSingleInvocation("Bombing", instanceData, instanceNumber, instanceId, false);
4677
+ let attempt;
4678
+ try {
4679
+ attempt = await runSingleInvocation("Bombing", instanceData, instanceNumber, instanceId, false);
4680
+ }
4681
+ catch (error) {
4682
+ if (error instanceof RuntimePolicyCallbackError) {
4683
+ stopScenario = true;
4684
+ scenarioAbortController.abort(error);
4685
+ }
4686
+ throw error;
4687
+ }
3884
4688
  const shouldRetry = !attempt.reply.isSuccess
3885
4689
  && scenario.shouldRestartIterationOnFail()
3886
4690
  && attempts < maxAttempts
3887
4691
  && !shouldStopNow();
4692
+ captureAttemptObservation("Bombing", globalOrdinal, attempts - 1, !shouldRetry, attempt, simulationIndex, simulationKind, iterationId, explicitSecondaryOrdinal);
3888
4693
  if (!shouldRetry) {
4694
+ for (const step of attempt.recordedSteps) {
4695
+ recordStepReply(step.stepName, step.reply, step.observedLatencyMs, step.sortIndex);
4696
+ }
3889
4697
  recordScenarioReply(attempt.reply, attempt.observedLatencyMs);
3890
4698
  if (scenario.getMaxFailCount() > 0 && runtime.allFailCount >= scenario.getMaxFailCount()) {
3891
4699
  stopScenario = true;
@@ -3905,30 +4713,282 @@ async function executeScenarioRuntime(args) {
3905
4713
  const endMs = Date.now() + Math.trunc(warmUpDurationSeconds * 1000);
3906
4714
  const instanceInfo = nextInstanceInfo();
3907
4715
  while (Date.now() < endMs && !shouldStopNow()) {
3908
- await runSingleInvocation("WarmUp", instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId, false);
4716
+ const globalOrdinal = nextObservationOrdinal();
4717
+ const attempt = await runSingleInvocation("WarmUp", instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId, false);
4718
+ captureAttemptObservation("WarmUp", globalOrdinal, 0, true, attempt, -1, "SingleInvocation");
3909
4719
  }
3910
4720
  };
3911
- const executeSimulationAsync = async (simulation) => {
3912
- const kind = String(simulation.Kind ?? "");
3913
- const weight = Math.max(scenario.getWeight(), 1);
3914
- const rate = applyScenarioWeight(toInt(simulation.Rate), weight);
3915
- const minRate = applyScenarioWeight(toInt(simulation.MinRate), weight);
3916
- const maxRate = applyScenarioWeight(toInt(simulation.MaxRate), weight);
3917
- const copies = Math.max(applyScenarioWeight(Math.max(toInt(simulation.Copies), 1), weight), 1);
3918
- const iterations = applyScenarioWeight(Math.max(toInt(simulation.Iterations), 0), weight);
3919
- const intervalMs = Math.max(Math.trunc(Math.max(toNumber(simulation.IntervalSeconds), 0) * 1000), 0);
3920
- const duringMs = Math.max(Math.trunc(Math.max(toNumber(simulation.DuringSeconds), 0) * 1000), 0);
3921
- accumulator.setLoadSimulation(kind, resolveLoadSimulationValue(simulation, weight));
4721
+ const executeV2TimedConstant = async (copies, durationNs, ramping, runSimulationInvocation, segment) => {
4722
+ if (!loadEngineV2Budget) {
4723
+ return;
4724
+ }
4725
+ const offsets = ramping ? (0, load_engine_v2_js_1.planRampingConstantDeadlines)(copies, durationNs) : Array(copies).fill(0n);
4726
+ const ownedWorkerSlots = trafficMixV2
4727
+ ? (0, load_engine_v2_js_1.loadEngineV2TrafficMixOwnedUnits)(BigInt(copies), trafficMixV2.shareWeights, trafficMixV2.laneIndex, scenarioPartition.number, scenarioPartition.count)
4728
+ : offsets.flatMap((_offset, workerSlot) => workerSlot % scenarioPartition.count === scenarioPartition.number
4729
+ ? [{ laneOrdinal: BigInt(workerSlot), globalRank: BigInt(workerSlot) }]
4730
+ : []);
4731
+ const quantum = ramping ? maxBigInt(1n, durationNs / BigInt(copies)) : 1n;
4732
+ const toleranceNs = minBigInt(100000000n, maxBigInt(2000000n, quantum * 4n));
4733
+ const startNs = process.hrtime.bigint();
4734
+ const endNs = startNs + durationNs;
4735
+ const active = new Set();
4736
+ let schedulerLate = 0;
4737
+ let unavailable = 0;
4738
+ let executionError;
4739
+ if (segment) {
4740
+ segment.requestedWorkers = BigInt(ownedWorkerSlots.length);
4741
+ }
4742
+ for (const unit of ownedWorkerSlots) {
4743
+ const offsetNs = offsets[Number(unit.globalRank)];
4744
+ await delayUntilMonotonicDeadline(startNs + offsetNs, scenarioCancellationToken);
4745
+ if (shouldStopNow()) {
4746
+ break;
4747
+ }
4748
+ const decisionNowNs = process.hrtime.bigint();
4749
+ if (segment)
4750
+ loadEngineV2Telemetry?.recordDecisionLag(segment, decisionNowNs - (startNs + offsetNs));
4751
+ if (decisionNowNs - (startNs + offsetNs) > toleranceNs) {
4752
+ schedulerLate += 1;
4753
+ if (segment) {
4754
+ segment.unavailableWorkers += 1n;
4755
+ incrementReason(segment.unavailableWorkerReasons, "scheduler_late");
4756
+ }
4757
+ continue;
4758
+ }
4759
+ const release = loadEngineV2Budget.tryAcquire();
4760
+ if (!release) {
4761
+ unavailable += 1;
4762
+ if (segment) {
4763
+ segment.unavailableWorkers += 1n;
4764
+ incrementReason(segment.unavailableWorkerReasons, "max_in_flight");
4765
+ }
4766
+ continue;
4767
+ }
4768
+ if (segment)
4769
+ segment.startedWorkers += 1n;
4770
+ const instanceInfo = nextInstanceInfo();
4771
+ let task;
4772
+ task = (async () => {
4773
+ if (segment) {
4774
+ loadEngineV2Telemetry?.recordStartLag(segment, process.hrtime.bigint() - (startNs + offsetNs));
4775
+ }
4776
+ let completedSinceYield = 0;
4777
+ let workerIterationIndex = 0n;
4778
+ while (process.hrtime.bigint() < endNs && !shouldStopNow()) {
4779
+ if (segment) {
4780
+ segment.planned += 1n;
4781
+ segment.due += 1n;
4782
+ segment.started += 1n;
4783
+ }
4784
+ await runSimulationInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId, unit.globalRank, workerIterationIndex);
4785
+ workerIterationIndex += 1n;
4786
+ if (segment)
4787
+ segment.completed += 1n;
4788
+ completedSinceYield += 1;
4789
+ if (completedSinceYield >= 64) {
4790
+ completedSinceYield = 0;
4791
+ await new Promise((resolve) => setImmediate(resolve));
4792
+ }
4793
+ }
4794
+ })().catch((error) => {
4795
+ executionError ?? (executionError = error);
4796
+ }).finally(() => {
4797
+ release();
4798
+ active.delete(task);
4799
+ });
4800
+ active.add(task);
4801
+ }
4802
+ while (active.size > 0) {
4803
+ await Promise.race(active);
4804
+ }
4805
+ if (segment) {
4806
+ segment.accountingComplete = segment.completed === segment.started
4807
+ && segment.started === segment.due
4808
+ && segment.due === segment.planned
4809
+ && segment.startedWorkers + segment.unavailableWorkers === segment.requestedWorkers;
4810
+ loadEngineV2Telemetry?.recordWarning("scheduler_late", segment, BigInt(schedulerLate));
4811
+ loadEngineV2Telemetry?.recordWarning("max_in_flight", segment, BigInt(unavailable));
4812
+ }
4813
+ if (schedulerLate > 0) {
4814
+ logger.warn(`Load Engine V2 warning scheduler_late: ${schedulerLate} constant worker slots were unavailable for scenario ${scenario.name}.`);
4815
+ }
4816
+ if (unavailable > 0) {
4817
+ logger.warn(`Load Engine V2 warning max_in_flight: ${unavailable} constant worker slots were unavailable for scenario ${scenario.name}.`);
4818
+ }
4819
+ if (executionError !== undefined) {
4820
+ throw executionError;
4821
+ }
4822
+ };
4823
+ const executeV2IterationsConstant = async (copies, iterations, runSimulationInvocation, segment) => {
4824
+ if (!loadEngineV2Budget || iterations <= 0) {
4825
+ return;
4826
+ }
4827
+ let activeShardCount;
4828
+ let ownedWorkerSlots;
4829
+ let ownedIterations;
4830
+ if (trafficMixV2) {
4831
+ const laneCopies = (0, load_engine_v2_js_1.loadEngineV2TrafficMixLaneUnitCount)(BigInt(copies), trafficMixV2.shareWeights, trafficMixV2.laneIndex);
4832
+ const laneIterations = (0, load_engine_v2_js_1.loadEngineV2TrafficMixLaneUnitCount)(BigInt(iterations), trafficMixV2.shareWeights, trafficMixV2.laneIndex);
4833
+ if (laneIterations === 0n) {
4834
+ if (segment) {
4835
+ segment.requestedWorkers = 0n;
4836
+ segment.accountingComplete = true;
4837
+ }
4838
+ return;
4839
+ }
4840
+ if (laneCopies === 0n) {
4841
+ throw new Error("Load Engine V2 traffic-mix lane owns iterations but no constant worker slot.");
4842
+ }
4843
+ activeShardCount = Math.min(scenarioPartition.count, Number(laneCopies), Number(laneIterations));
4844
+ if (scenarioPartition.number >= activeShardCount) {
4845
+ if (segment) {
4846
+ segment.requestedWorkers = 0n;
4847
+ segment.accountingComplete = true;
4848
+ }
4849
+ return;
4850
+ }
4851
+ ownedWorkerSlots = (0, load_engine_v2_js_1.loadEngineV2TrafficMixOwnedUnits)(BigInt(copies), trafficMixV2.shareWeights, trafficMixV2.laneIndex, scenarioPartition.number, activeShardCount);
4852
+ ownedIterations = (0, load_engine_v2_js_1.loadEngineV2TrafficMixOwnedUnits)(BigInt(iterations), trafficMixV2.shareWeights, trafficMixV2.laneIndex, scenarioPartition.number, activeShardCount);
4853
+ }
4854
+ else {
4855
+ activeShardCount = Math.min(scenarioPartition.count, copies, iterations);
4856
+ if (scenarioPartition.number >= activeShardCount) {
4857
+ return;
4858
+ }
4859
+ ownedWorkerSlots = Array.from({ length: copies }, (_value, slot) => slot)
4860
+ .filter((slot) => slot % activeShardCount === scenarioPartition.number)
4861
+ .map((slot) => ({ laneOrdinal: BigInt(slot), globalRank: BigInt(slot) }));
4862
+ ownedIterations = Array.from({ length: iterations }, (_value, ordinal) => ordinal)
4863
+ .filter((ordinal) => ordinal % activeShardCount === scenarioPartition.number)
4864
+ .map((ordinal) => ({ laneOrdinal: BigInt(ordinal), globalRank: BigInt(ordinal) }));
4865
+ }
4866
+ if (scenarioPartition.number >= activeShardCount) {
4867
+ return;
4868
+ }
4869
+ const targetWorkers = Math.min(ownedWorkerSlots.length, ownedIterations.length);
4870
+ if (targetWorkers <= 0) {
4871
+ if (segment) {
4872
+ segment.requestedWorkers = 0n;
4873
+ segment.accountingComplete = true;
4874
+ }
4875
+ return;
4876
+ }
4877
+ if (segment) {
4878
+ segment.planned = BigInt(ownedIterations.length);
4879
+ segment.requestedWorkers = BigInt(targetWorkers);
4880
+ }
4881
+ const releases = [];
4882
+ let firstRelease;
4883
+ while (!firstRelease && !shouldStopNow()) {
4884
+ firstRelease = loadEngineV2Budget.tryAcquire();
4885
+ if (!firstRelease) {
4886
+ await delayWithAbort(1, scenarioCancellationToken);
4887
+ }
4888
+ }
4889
+ if (!firstRelease) {
4890
+ if (segment) {
4891
+ segment.unreached = segment.planned;
4892
+ segment.unavailableWorkers = segment.requestedWorkers;
4893
+ if (segment.unavailableWorkers > 0n) {
4894
+ incrementReason(segment.unavailableWorkerReasons, "cancelled", segment.unavailableWorkers);
4895
+ }
4896
+ segment.accountingComplete = true;
4897
+ }
4898
+ return;
4899
+ }
4900
+ releases.push(firstRelease);
4901
+ for (let worker = 1; worker < targetWorkers; worker += 1) {
4902
+ const release = loadEngineV2Budget.tryAcquire();
4903
+ if (release) {
4904
+ releases.push(release);
4905
+ }
4906
+ }
4907
+ if (releases.length < targetWorkers) {
4908
+ logger.warn(`Load Engine V2 warning max_in_flight: requested ${targetWorkers} constant workers but started ${releases.length} for scenario ${scenario.name}; all iterations will still run.`);
4909
+ }
4910
+ if (segment) {
4911
+ segment.startedWorkers = BigInt(releases.length);
4912
+ segment.unavailableWorkers = BigInt(targetWorkers - releases.length);
4913
+ if (segment.unavailableWorkers > 0n) {
4914
+ incrementReason(segment.unavailableWorkerReasons, "max_in_flight", segment.unavailableWorkers);
4915
+ loadEngineV2Telemetry?.recordWarning("max_in_flight", segment, segment.unavailableWorkers);
4916
+ }
4917
+ }
4918
+ let nextIterationIndex = 0;
4919
+ const tasks = releases.map(async (release) => {
4920
+ const instanceInfo = nextInstanceInfo();
4921
+ try {
4922
+ while (!shouldStopNow()) {
4923
+ const unit = ownedIterations[nextIterationIndex];
4924
+ nextIterationIndex += 1;
4925
+ if (!unit) {
4926
+ break;
4927
+ }
4928
+ if (segment) {
4929
+ segment.due += 1n;
4930
+ segment.started += 1n;
4931
+ }
4932
+ await runSimulationInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId, unit.globalRank, 0n);
4933
+ if (segment)
4934
+ segment.completed += 1n;
4935
+ }
4936
+ }
4937
+ finally {
4938
+ release();
4939
+ }
4940
+ });
4941
+ await waitForScenarioTasks(tasks, scenario.name, scenarioCompletionTimeoutSeconds, logger, scenarioCancellationToken);
4942
+ if (segment) {
4943
+ segment.unreached = segment.planned > segment.due ? segment.planned - segment.due : 0n;
4944
+ segment.accountingComplete = segment.due + segment.unreached === segment.planned
4945
+ && segment.started === segment.due
4946
+ && segment.completed === segment.started
4947
+ && segment.startedWorkers + segment.unavailableWorkers === segment.requestedWorkers;
4948
+ }
4949
+ };
4950
+ const executeSimulationAsync = async (simulation, simulationIndex) => {
4951
+ const descriptor = trafficMixV2
4952
+ ? trafficMixV2.originalSimulations[simulationIndex]
4953
+ : simulation;
4954
+ if (!descriptor) {
4955
+ throw new Error(`Load Engine V2 traffic-mix phase ${simulationIndex} is missing its global descriptor.`);
4956
+ }
4957
+ const kind = String(descriptor.Kind ?? "");
4958
+ const simulationKind = kind || "SingleInvocation";
4959
+ const runSimulationInvocation = (instanceData, instanceNumber, instanceId, explicitGlobalOrdinal, explicitSecondaryOrdinal = 0n) => runBombingInvocation(instanceData, instanceNumber, instanceId, explicitGlobalOrdinal, simulationIndex, simulationKind, explicitSecondaryOrdinal);
4960
+ if (options.loadEngineContractVersion === 2) {
4961
+ nextV2FallbackOrdinal = BigInt(scenarioPartition.number);
4962
+ }
4963
+ const weight = trafficMixV2 ? 1 : Math.max(scenario.getWeight(), 1);
4964
+ const rate = applyScenarioWeight(toInt(descriptor.Rate), weight);
4965
+ const minRate = applyScenarioWeight(toInt(descriptor.MinRate), weight);
4966
+ const maxRate = applyScenarioWeight(toInt(descriptor.MaxRate), weight);
4967
+ const copies = Math.max(applyScenarioWeight(Math.max(toInt(descriptor.Copies), 1), weight), 1);
4968
+ const iterations = applyScenarioWeight(Math.max(toInt(descriptor.Iterations), 0), weight);
4969
+ const intervalMs = Math.max(Math.trunc(Math.max(toNumber(descriptor.IntervalSeconds), 0) * 1000), 0);
4970
+ const duringMs = Math.max(Math.trunc(Math.max(toNumber(descriptor.DuringSeconds), 0) * 1000), 0);
4971
+ accumulator.setLoadSimulation(kind, resolveLoadSimulationValue(descriptor, weight));
3922
4972
  accumulator.setCurrentOperation("Bombing");
4973
+ const schedulerSegment = options.loadEngineContractVersion === 2
4974
+ ? loadEngineV2Telemetry?.createSegment(scenario.name, scenarioIndex, simulationIndex, kind, scenarioPartition.number, scenarioPartition.count)
4975
+ : undefined;
4976
+ const trafficMixOwnedOrdinals = (total) => trafficMixV2
4977
+ ? (0, load_engine_v2_js_1.loadEngineV2TrafficMixOwnedUnits)(total, trafficMixV2.shareWeights, trafficMixV2.laneIndex, scenarioPartition.number, scenarioPartition.count).map((unit) => unit.globalRank)
4978
+ : undefined;
3923
4979
  if (kind === "KeepConstant") {
3924
4980
  if (duringMs <= 0) {
3925
4981
  return;
3926
4982
  }
4983
+ if (options.loadEngineContractVersion === 2 && loadEngineV2Budget) {
4984
+ await executeV2TimedConstant(copies, secondsToNanoseconds(toNumber(descriptor.DuringSeconds), "KeepConstant duration"), false, runSimulationInvocation, schedulerSegment);
4985
+ return;
4986
+ }
3927
4987
  const endMs = Date.now() + duringMs;
3928
4988
  const tasks = Array.from({ length: copies }, async () => {
3929
4989
  const instanceInfo = nextInstanceInfo();
3930
4990
  while (Date.now() < endMs && !shouldStopNow()) {
3931
- await runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId);
4991
+ await runSimulationInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId);
3932
4992
  }
3933
4993
  });
3934
4994
  await waitForScenarioTasks(tasks, scenario.name, scenarioCompletionTimeoutSeconds, logger, scenarioCancellationToken);
@@ -3938,6 +4998,10 @@ async function executeScenarioRuntime(args) {
3938
4998
  if (duringMs <= 0) {
3939
4999
  return;
3940
5000
  }
5001
+ if (options.loadEngineContractVersion === 2 && loadEngineV2Budget) {
5002
+ await executeV2TimedConstant(copies, secondsToNanoseconds(toNumber(descriptor.DuringSeconds), "RampingConstant duration"), true, runSimulationInvocation, schedulerSegment);
5003
+ return;
5004
+ }
3941
5005
  const tasks = [];
3942
5006
  const endMs = Date.now() + duringMs;
3943
5007
  const startIntervalMs = copies <= 1 ? 0 : Math.max(Math.trunc(duringMs / copies), 0);
@@ -3945,7 +5009,7 @@ async function executeScenarioRuntime(args) {
3945
5009
  const instanceInfo = nextInstanceInfo();
3946
5010
  tasks.push((async () => {
3947
5011
  while (Date.now() < endMs && !shouldStopNow()) {
3948
- await runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId);
5012
+ await runSimulationInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId);
3949
5013
  }
3950
5014
  })());
3951
5015
  if (copy < copies && startIntervalMs > 0) {
@@ -3959,12 +5023,34 @@ async function executeScenarioRuntime(args) {
3959
5023
  if (duringMs <= 0 || rate <= 0 || intervalMs <= 0) {
3960
5024
  return;
3961
5025
  }
5026
+ if (options.loadEngineContractVersion === 2 && loadEngineV2Budget) {
5027
+ const intervalNs = secondsToNanoseconds(toNumber(descriptor.IntervalSeconds), "Inject interval");
5028
+ const durationNs = secondsToNanoseconds(toNumber(descriptor.DuringSeconds), "Inject duration");
5029
+ await executeV2FixedArrivals({
5030
+ rate,
5031
+ intervalNs,
5032
+ totalArrivals: (0, load_engine_v2_js_1.loadEngineV2FixedArrivalCount)(rate, intervalNs, durationNs),
5033
+ budget: loadEngineV2Budget,
5034
+ cancellationToken: scenarioCancellationToken,
5035
+ shouldStopNow,
5036
+ nextInstanceInfo,
5037
+ runBombingInvocation: runSimulationInvocation,
5038
+ logger,
5039
+ scenarioName: scenario.name,
5040
+ shardIndex: scenarioPartition.number,
5041
+ shardCount: scenarioPartition.count,
5042
+ ownedOrdinals: trafficMixOwnedOrdinals((0, load_engine_v2_js_1.loadEngineV2FixedArrivalCount)(rate, intervalNs, durationNs)),
5043
+ telemetry: loadEngineV2Telemetry,
5044
+ segment: schedulerSegment
5045
+ });
5046
+ return;
5047
+ }
3962
5048
  const pending = [];
3963
5049
  const endMs = Date.now() + duringMs;
3964
5050
  while (Date.now() < endMs && !shouldStopNow()) {
3965
5051
  for (let index = 0; index < rate; index += 1) {
3966
5052
  const instanceInfo = nextInstanceInfo();
3967
- pending.push(runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId));
5053
+ pending.push(runSimulationInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId));
3968
5054
  }
3969
5055
  await delayWithAbort(intervalMs, scenarioCancellationToken);
3970
5056
  }
@@ -3975,6 +5061,31 @@ async function executeScenarioRuntime(args) {
3975
5061
  if (duringMs <= 0 || rate <= 0 || intervalMs <= 0) {
3976
5062
  return;
3977
5063
  }
5064
+ if (options.loadEngineContractVersion === 2 && loadEngineV2Budget) {
5065
+ const intervalNs = secondsToNanoseconds(toNumber(descriptor.IntervalSeconds), "RampingInject interval");
5066
+ const durationNs = secondsToNanoseconds(toNumber(descriptor.DuringSeconds), "RampingInject duration");
5067
+ const offsets = (0, load_engine_v2_js_1.planRampingInjectionDeadlines)(rate, intervalNs, durationNs);
5068
+ await executeV2FixedArrivals({
5069
+ rate,
5070
+ intervalNs,
5071
+ totalArrivals: BigInt(offsets.length),
5072
+ deadlineOffsetsNs: offsets,
5073
+ tolerancesNs: buildV2RampTolerances(offsets, durationNs),
5074
+ budget: loadEngineV2Budget,
5075
+ cancellationToken: scenarioCancellationToken,
5076
+ shouldStopNow,
5077
+ nextInstanceInfo,
5078
+ runBombingInvocation: runSimulationInvocation,
5079
+ logger,
5080
+ scenarioName: scenario.name,
5081
+ shardIndex: scenarioPartition.number,
5082
+ shardCount: scenarioPartition.count,
5083
+ ownedOrdinals: trafficMixOwnedOrdinals(BigInt(offsets.length)),
5084
+ telemetry: loadEngineV2Telemetry,
5085
+ segment: schedulerSegment
5086
+ });
5087
+ return;
5088
+ }
3978
5089
  const pending = [];
3979
5090
  const startedAtMs = Date.now();
3980
5091
  const endMs = startedAtMs + duringMs;
@@ -3984,7 +5095,7 @@ async function executeScenarioRuntime(args) {
3984
5095
  const currentRate = Math.max(1, Math.ceil(rate * progress));
3985
5096
  for (let index = 0; index < currentRate; index += 1) {
3986
5097
  const instanceInfo = nextInstanceInfo();
3987
- pending.push(runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId));
5098
+ pending.push(runSimulationInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId));
3988
5099
  }
3989
5100
  await delayWithAbort(intervalMs, scenarioCancellationToken);
3990
5101
  }
@@ -3995,6 +5106,37 @@ async function executeScenarioRuntime(args) {
3995
5106
  if (duringMs <= 0 || maxRate <= 0 || intervalMs <= 0) {
3996
5107
  return;
3997
5108
  }
5109
+ if (options.loadEngineContractVersion === 2 && loadEngineV2Budget) {
5110
+ const intervalNs = secondsToNanoseconds(toNumber(descriptor.IntervalSeconds), "InjectRandom interval");
5111
+ const durationNs = secondsToNanoseconds(toNumber(descriptor.DuringSeconds), "InjectRandom duration");
5112
+ const descriptorSeed = (0, load_engine_v2_js_1.fnv1a32)(trafficMixV2
5113
+ ? `traffic-mix\n${trafficMixV2.seedId}\n${simulationIndex}`
5114
+ : `${scenario.name}\n${simulationIndex}`);
5115
+ const rotated = ((descriptorSeed << 13) | (descriptorSeed >>> 19)) >>> 0;
5116
+ const seed = ((0, load_engine_v2_js_1.fnv1a32)(testInfo.sessionId) ^ rotated) >>> 0;
5117
+ const offsets = (0, load_engine_v2_js_1.planRandomInjectionDeadlines)(Math.max(0, minRate), maxRate, intervalNs, durationNs, seed);
5118
+ const tolerance = (0, load_engine_v2_js_1.loadEngineV2LatenessToleranceNs)(Math.max(1, maxRate), intervalNs);
5119
+ await executeV2FixedArrivals({
5120
+ rate: Math.max(1, maxRate),
5121
+ intervalNs,
5122
+ totalArrivals: BigInt(offsets.length),
5123
+ deadlineOffsetsNs: offsets,
5124
+ tolerancesNs: offsets.map(() => tolerance),
5125
+ budget: loadEngineV2Budget,
5126
+ cancellationToken: scenarioCancellationToken,
5127
+ shouldStopNow,
5128
+ nextInstanceInfo,
5129
+ runBombingInvocation: runSimulationInvocation,
5130
+ logger,
5131
+ scenarioName: scenario.name,
5132
+ shardIndex: scenarioPartition.number,
5133
+ shardCount: scenarioPartition.count,
5134
+ ownedOrdinals: trafficMixOwnedOrdinals(BigInt(offsets.length)),
5135
+ telemetry: loadEngineV2Telemetry,
5136
+ segment: schedulerSegment
5137
+ });
5138
+ return;
5139
+ }
3998
5140
  const pending = [];
3999
5141
  const normalizedMinRate = Math.max(1, minRate);
4000
5142
  const normalizedMaxRate = Math.max(normalizedMinRate, maxRate);
@@ -4003,7 +5145,7 @@ async function executeScenarioRuntime(args) {
4003
5145
  const currentRate = randomIntInclusive(normalizedMinRate, normalizedMaxRate);
4004
5146
  for (let index = 0; index < currentRate; index += 1) {
4005
5147
  const instanceInfo = nextInstanceInfo();
4006
- pending.push(runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId));
5148
+ pending.push(runSimulationInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId));
4007
5149
  }
4008
5150
  await delayWithAbort(intervalMs, scenarioCancellationToken);
4009
5151
  }
@@ -4014,6 +5156,10 @@ async function executeScenarioRuntime(args) {
4014
5156
  if (iterations <= 0) {
4015
5157
  return;
4016
5158
  }
5159
+ if (options.loadEngineContractVersion === 2 && loadEngineV2Budget) {
5160
+ await executeV2IterationsConstant(copies, iterations, runSimulationInvocation, schedulerSegment);
5161
+ return;
5162
+ }
4017
5163
  let remaining = iterations;
4018
5164
  const tasks = Array.from({ length: copies }, async () => {
4019
5165
  const instanceInfo = nextInstanceInfo();
@@ -4022,7 +5168,7 @@ async function executeScenarioRuntime(args) {
4022
5168
  if (remaining < 0) {
4023
5169
  break;
4024
5170
  }
4025
- await runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId);
5171
+ await runSimulationInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId);
4026
5172
  }
4027
5173
  });
4028
5174
  await waitForScenarioTasks(tasks, scenario.name, scenarioCompletionTimeoutSeconds, logger, scenarioCancellationToken);
@@ -4032,13 +5178,33 @@ async function executeScenarioRuntime(args) {
4032
5178
  if (iterations <= 0 || rate <= 0 || intervalMs <= 0) {
4033
5179
  return;
4034
5180
  }
5181
+ if (options.loadEngineContractVersion === 2 && loadEngineV2Budget) {
5182
+ await executeV2FixedArrivals({
5183
+ rate,
5184
+ intervalNs: secondsToNanoseconds(toNumber(descriptor.IntervalSeconds), "IterationsForInject interval"),
5185
+ totalArrivals: BigInt(iterations),
5186
+ budget: loadEngineV2Budget,
5187
+ cancellationToken: scenarioCancellationToken,
5188
+ shouldStopNow,
5189
+ nextInstanceInfo,
5190
+ runBombingInvocation: runSimulationInvocation,
5191
+ logger,
5192
+ scenarioName: scenario.name,
5193
+ shardIndex: scenarioPartition.number,
5194
+ shardCount: scenarioPartition.count,
5195
+ ownedOrdinals: trafficMixOwnedOrdinals(BigInt(iterations)),
5196
+ telemetry: loadEngineV2Telemetry,
5197
+ segment: schedulerSegment
5198
+ });
5199
+ return;
5200
+ }
4035
5201
  const pending = [];
4036
5202
  let remaining = iterations;
4037
5203
  while (remaining > 0 && !shouldStopNow()) {
4038
5204
  const count = Math.min(rate, remaining);
4039
5205
  for (let index = 0; index < count; index += 1) {
4040
5206
  const instanceInfo = nextInstanceInfo();
4041
- pending.push(runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId));
5207
+ pending.push(runSimulationInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId));
4042
5208
  }
4043
5209
  remaining -= count;
4044
5210
  if (remaining > 0) {
@@ -4050,10 +5216,19 @@ async function executeScenarioRuntime(args) {
4050
5216
  }
4051
5217
  if (kind === "Pause") {
4052
5218
  if (duringMs > 0) {
4053
- await delayWithAbort(duringMs, scenarioCancellationToken);
5219
+ if (options.loadEngineContractVersion === 2) {
5220
+ await delayUntilMonotonicDeadline(process.hrtime.bigint() + secondsToNanoseconds(toNumber(descriptor.DuringSeconds), "Pause duration"), scenarioCancellationToken);
5221
+ }
5222
+ else {
5223
+ await delayWithAbort(duringMs, scenarioCancellationToken);
5224
+ }
4054
5225
  }
5226
+ if (schedulerSegment)
5227
+ schedulerSegment.accountingComplete = true;
4055
5228
  return;
4056
5229
  }
5230
+ if (schedulerSegment)
5231
+ schedulerSegment.accountingComplete = true;
4057
5232
  };
4058
5233
  const initContext = {
4059
5234
  customSettings: { ...(options.customSettings ?? {}) },
@@ -4085,15 +5260,33 @@ async function executeScenarioRuntime(args) {
4085
5260
  accumulator.setCurrentOperation("Bombing");
4086
5261
  const simulations = scenario.getSimulations();
4087
5262
  if (!simulations.length) {
4088
- const instanceInfo = nextInstanceInfo();
4089
- await runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId);
5263
+ if (options.loadEngineContractVersion === 2) {
5264
+ await options.loadEngineV2SegmentLifecycleOverride?.beforeSegment(scenarioIndex, 0);
5265
+ try {
5266
+ await executeSimulationAsync(LoadStrikeSimulation.iterationsForConstant(1, 1), 0);
5267
+ }
5268
+ finally {
5269
+ await options.loadEngineV2SegmentLifecycleOverride?.afterSegment(scenarioIndex, 0);
5270
+ }
5271
+ }
5272
+ else {
5273
+ const instanceInfo = nextInstanceInfo();
5274
+ await runBombingInvocation(instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId);
5275
+ }
4090
5276
  }
4091
5277
  else {
4092
- for (const simulation of simulations) {
5278
+ for (let simulationIndex = 0; simulationIndex < simulations.length; simulationIndex += 1) {
5279
+ const simulation = simulations[simulationIndex];
4093
5280
  if (shouldStopNow()) {
4094
5281
  break;
4095
5282
  }
4096
- await executeSimulationAsync(simulation);
5283
+ await options.loadEngineV2SegmentLifecycleOverride?.beforeSegment(scenarioIndex, simulationIndex);
5284
+ try {
5285
+ await executeSimulationAsync(simulation, simulationIndex);
5286
+ }
5287
+ finally {
5288
+ await options.loadEngineV2SegmentLifecycleOverride?.afterSegment(scenarioIndex, simulationIndex);
5289
+ }
4097
5290
  }
4098
5291
  }
4099
5292
  accumulator.setCurrentOperation(stopScenario || stopTestState.value ? "Stop" : "Complete");
@@ -4182,18 +5375,29 @@ async function waitForScenarioTasks(tasks, scenarioName, timeoutSeconds, logger,
4182
5375
  if (!tasks.length) {
4183
5376
  return;
4184
5377
  }
4185
- const all = Promise.allSettled(tasks).then(() => { });
5378
+ const settled = Promise.allSettled(tasks);
5379
+ const throwPolicyFailure = (results) => {
5380
+ const failure = results.find((result) => result.status === "rejected" && result.reason instanceof RuntimePolicyCallbackError);
5381
+ if (failure) {
5382
+ throw failure.reason;
5383
+ }
5384
+ };
4186
5385
  if (timeoutSeconds <= 0) {
4187
- await all;
5386
+ throwPolicyFailure(await settled);
4188
5387
  return;
4189
5388
  }
4190
5389
  const completed = await Promise.race([
4191
- all.then(() => true),
5390
+ settled.then(() => true),
4192
5391
  delayWithAbort(Math.trunc(timeoutSeconds * 1000), signal).then(() => false)
4193
5392
  ]);
4194
5393
  if (!completed) {
5394
+ if (signal.reason instanceof RuntimePolicyCallbackError) {
5395
+ throw signal.reason;
5396
+ }
4195
5397
  logger.warn(`Scenario ${scenarioName} timed out while waiting for completion (${timeoutSeconds}s).`);
5398
+ return;
4196
5399
  }
5400
+ throwPolicyFailure(await settled);
4197
5401
  }
4198
5402
  function randomIntInclusive(minValue, maxValue) {
4199
5403
  const min = Math.trunc(Math.min(minValue, maxValue));
@@ -4420,6 +5624,9 @@ function normalizeDataTransferStatsValue(value) {
4420
5624
  const source = asAliasRecord(value);
4421
5625
  return {
4422
5626
  allBytes: pickAliasNumber(source, "allBytes", "AllBytes"),
5627
+ ...(hasAliasValue(source, "allBytes64", "AllBytes64")
5628
+ ? { allBytes64: pickAliasString(source, "allBytes64", "AllBytes64") }
5629
+ : {}),
4423
5630
  maxBytes: pickAliasNumber(source, "maxBytes", "MaxBytes"),
4424
5631
  meanBytes: pickAliasNumber(source, "meanBytes", "MeanBytes"),
4425
5632
  minBytes: pickAliasNumber(source, "minBytes", "MinBytes"),
@@ -4427,6 +5634,9 @@ function normalizeDataTransferStatsValue(value) {
4427
5634
  percent75: pickAliasNumber(source, "percent75", "Percent75"),
4428
5635
  percent95: pickAliasNumber(source, "percent95", "Percent95"),
4429
5636
  percent99: pickAliasNumber(source, "percent99", "Percent99"),
5637
+ ...(hasAliasValue(source, "percent100", "Percent100")
5638
+ ? { percent100: pickAliasNumber(source, "percent100", "Percent100") }
5639
+ : {}),
4430
5640
  stdDev: pickAliasNumber(source, "stdDev", "StdDev")
4431
5641
  };
4432
5642
  }
@@ -4449,6 +5659,9 @@ function normalizeLatencyStatsValue(value) {
4449
5659
  percent75: pickAliasNumber(source, "percent75", "Percent75"),
4450
5660
  percent95: pickAliasNumber(source, "percent95", "Percent95"),
4451
5661
  percent99: pickAliasNumber(source, "percent99", "Percent99"),
5662
+ ...(hasAliasValue(source, "percent100", "Percent100")
5663
+ ? { percent100: pickAliasNumber(source, "percent100", "Percent100") }
5664
+ : {}),
4452
5665
  stdDev: pickAliasNumber(source, "stdDev", "StdDev")
4453
5666
  };
4454
5667
  }
@@ -4464,13 +5677,42 @@ function normalizeStatusCodeStatsValue(value) {
4464
5677
  }
4465
5678
  function normalizeMeasurementStatsValue(value) {
4466
5679
  const source = asAliasRecord(value);
4467
- return {
5680
+ const projected = {
5681
+ ...(hasAliasValue(source, "count64", "Count64")
5682
+ ? { count64: pickAliasString(source, "count64", "Count64") }
5683
+ : {}),
5684
+ ...(hasAliasValue(source, "distributionMode", "DistributionMode")
5685
+ ? { distributionMode: pickAliasString(source, "distributionMode", "DistributionMode") }
5686
+ : {}),
5687
+ ...(hasAliasValue(source, "maxRelativeError", "MaxRelativeError")
5688
+ ? { maxRelativeError: pickAliasNumber(source, "maxRelativeError", "MaxRelativeError") }
5689
+ : {}),
4468
5690
  dataTransfer: normalizeDataTransferStatsValue(pickAliasValue(source, "dataTransfer", "DataTransfer")),
4469
5691
  latency: normalizeLatencyStatsValue(pickAliasValue(source, "latency", "Latency")),
4470
5692
  request: normalizeRequestStatsValue(pickAliasValue(source, "request", "Request")),
4471
5693
  statusCodes: pickAliasArray(source, "statusCodes", "StatusCodes")
4472
5694
  .map((entry) => normalizeStatusCodeStatsValue(entry))
4473
5695
  };
5696
+ const sidecarValue = pickAliasValue(source, "histogramSidecar", "HistogramSidecar");
5697
+ if (sidecarValue && typeof sidecarValue === "object" && !Array.isArray(sidecarValue)) {
5698
+ const sidecar = sidecarValue;
5699
+ if (sidecar?.latency && sidecar.size) {
5700
+ Object.defineProperty(projected, "histogramSidecar", {
5701
+ value: {
5702
+ latency: load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(sidecar.latency).toSidecar(),
5703
+ size: load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(sidecar.size).toSidecar(),
5704
+ allBytes64: String(sidecar.allBytes64 ?? "0"),
5705
+ lessOrEq80064: String(sidecar.lessOrEq80064 ?? "0"),
5706
+ more800Less120064: String(sidecar.more800Less120064 ?? "0"),
5707
+ moreOrEq120064: String(sidecar.moreOrEq120064 ?? "0")
5708
+ },
5709
+ enumerable: false,
5710
+ configurable: false,
5711
+ writable: false
5712
+ });
5713
+ }
5714
+ }
5715
+ return projected;
4474
5716
  }
4475
5717
  function normalizeLoadSimulationStatsValue(value) {
4476
5718
  const source = asAliasRecord(value);
@@ -4570,6 +5812,9 @@ function normalizeStepStatsValue(value, index = 0) {
4570
5812
  statusCodes: normalizeAliasNumberRecord(pickAliasValue(source, "statusCodes", "StatusCodes")),
4571
5813
  ok: normalizeMeasurementStatsValue(pickAliasValue(source, "ok", "Ok")),
4572
5814
  fail: normalizeMeasurementStatsValue(pickAliasValue(source, "fail", "Fail")),
5815
+ ...(hasAliasValue(source, "allMeasurement", "AllMeasurement")
5816
+ ? { allMeasurement: normalizeMeasurementStatsValue(pickAliasValue(source, "allMeasurement", "AllMeasurement")) }
5817
+ : {}),
4573
5818
  sortIndex: hasAliasValue(source, "sortIndex", "SortIndex")
4574
5819
  ? pickAliasNumber(source, "sortIndex", "SortIndex")
4575
5820
  : index
@@ -4609,6 +5854,9 @@ function normalizeScenarioStatsValue(value, index = 0) {
4609
5854
  durationMs: pickAliasNumber(source, "durationMs", "DurationMs", "Duration"),
4610
5855
  ok: normalizeMeasurementStatsValue(pickAliasValue(source, "ok", "Ok")),
4611
5856
  fail: normalizeMeasurementStatsValue(pickAliasValue(source, "fail", "Fail")),
5857
+ ...(hasAliasValue(source, "allMeasurement", "AllMeasurement")
5858
+ ? { allMeasurement: normalizeMeasurementStatsValue(pickAliasValue(source, "allMeasurement", "AllMeasurement")) }
5859
+ : {}),
4612
5860
  loadSimulationStats: normalizeLoadSimulationStatsValue(pickAliasValue(source, "loadSimulationStats", "LoadSimulationStats")),
4613
5861
  sortIndex: hasAliasValue(source, "sortIndex", "SortIndex")
4614
5862
  ? pickAliasNumber(source, "sortIndex", "SortIndex")
@@ -4695,7 +5943,7 @@ function attachSessionStartInfoAliases(session) {
4695
5943
  return session;
4696
5944
  }
4697
5945
  function attachPortalReportingSession(sinkSession, sessionInfo, licenseClient, licenseSession) {
4698
- const runToken = stringValueOrDefault(licenseSession?.runToken, "").trim();
5946
+ const runToken = currentLicenseSessionRunToken(licenseSession);
4699
5947
  if (!runToken || !licenseClient) {
4700
5948
  return;
4701
5949
  }
@@ -4705,9 +5953,17 @@ function attachPortalReportingSession(sinkSession, sessionInfo, licenseClient, l
4705
5953
  sinkSession.portalReportingIngestUrl = ingestUrl;
4706
5954
  sinkSession.portalReportingRunId = runId;
4707
5955
  sessionInfo.runToken = runToken;
5956
+ Object.defineProperty(sessionInfo, PORTAL_RUN_TOKEN_PROVIDER, {
5957
+ configurable: true,
5958
+ enumerable: false,
5959
+ value: () => currentLicenseSessionRunToken(licenseSession)
5960
+ });
4708
5961
  sessionInfo.portalReportingIngestUrl = ingestUrl;
4709
5962
  sessionInfo.portalReportingRunId = runId;
4710
5963
  }
5964
+ function currentLicenseSessionRunToken(licenseSession) {
5965
+ return stringValueOrDefault(licenseSession?.runToken, "").trim();
5966
+ }
4711
5967
  function buildPortalReportingRunId(sessionId) {
4712
5968
  const sessionPart = String(sessionId ?? "")
4713
5969
  .replace(/[^A-Za-z0-9._-]+/g, "-")
@@ -4792,6 +6048,7 @@ function attachDataTransferStatsAliases(stats) {
4792
6048
  const projected = normalizeDataTransferStatsValue(stats);
4793
6049
  return attachAliasMap(projected, {
4794
6050
  AllBytes: "allBytes",
6051
+ AllBytes64: "allBytes64",
4795
6052
  MaxBytes: "maxBytes",
4796
6053
  MeanBytes: "meanBytes",
4797
6054
  MinBytes: "minBytes",
@@ -4799,6 +6056,7 @@ function attachDataTransferStatsAliases(stats) {
4799
6056
  Percent75: "percent75",
4800
6057
  Percent95: "percent95",
4801
6058
  Percent99: "percent99",
6059
+ Percent100: "percent100",
4802
6060
  StdDev: "stdDev"
4803
6061
  });
4804
6062
  }
@@ -4822,6 +6080,7 @@ function attachLatencyStatsAliases(stats) {
4822
6080
  Percent75: "percent75",
4823
6081
  Percent95: "percent95",
4824
6082
  Percent99: "percent99",
6083
+ Percent100: "percent100",
4825
6084
  StdDev: "stdDev"
4826
6085
  });
4827
6086
  return projected;
@@ -4843,6 +6102,9 @@ function attachMeasurementStatsAliases(stats) {
4843
6102
  projected.latency = attachLatencyStatsAliases(projected.latency);
4844
6103
  projected.statusCodes = projected.statusCodes.map((value) => attachStatusCodeStatsAliases(value));
4845
6104
  attachAliasMap(projected, {
6105
+ Count64: "count64",
6106
+ DistributionMode: "distributionMode",
6107
+ MaxRelativeError: "maxRelativeError",
4846
6108
  Request: "request",
4847
6109
  DataTransfer: "dataTransfer",
4848
6110
  Latency: "latency",
@@ -4904,6 +6166,8 @@ function attachStepStatsAliases(step) {
4904
6166
  const projected = normalizeStepStatsValue(step);
4905
6167
  projected.ok = attachMeasurementStatsAliases(projected.ok);
4906
6168
  projected.fail = attachMeasurementStatsAliases(projected.fail);
6169
+ if (projected.allMeasurement)
6170
+ projected.allMeasurement = attachMeasurementStatsAliases(projected.allMeasurement);
4907
6171
  attachAliasMap(projected, {
4908
6172
  ScenarioName: "scenarioName",
4909
6173
  StepName: "stepName",
@@ -4918,6 +6182,7 @@ function attachStepStatsAliases(step) {
4918
6182
  StatusCodes: "statusCodes",
4919
6183
  Ok: "ok",
4920
6184
  Fail: "fail",
6185
+ AllMeasurement: "allMeasurement",
4921
6186
  SortIndex: "sortIndex"
4922
6187
  });
4923
6188
  return projected;
@@ -4956,7 +6221,20 @@ function attachLoadSimulationProjection(simulation) {
4956
6221
  });
4957
6222
  return simulation;
4958
6223
  }
4959
- function expandTrafficMixScenarios(trafficMix) {
6224
+ function cloneLoadEngineV2TrafficMixMetadata(metadata) {
6225
+ return {
6226
+ ...metadata,
6227
+ shareWeights: [...metadata.shareWeights],
6228
+ originalSimulations: metadata.originalSimulations.map((simulation) => attachLoadSimulationProjection({ ...simulation }))
6229
+ };
6230
+ }
6231
+ function nextTrafficMixDeclarationIndex(scenarios) {
6232
+ return scenarios.reduce((next, scenario) => {
6233
+ const metadata = scenario.__loadStrikeTrafficMixV2Metadata();
6234
+ return metadata ? Math.max(next, metadata.declarationIndex + 1) : next;
6235
+ }, 0);
6236
+ }
6237
+ function expandTrafficMixScenarios(trafficMix, declarationIndex = 0) {
4960
6238
  if (!(trafficMix instanceof LoadStrikeTrafficMix)) {
4961
6239
  throw new TypeError("Traffic mix must be provided.");
4962
6240
  }
@@ -4969,16 +6247,30 @@ function expandTrafficMixScenarios(trafficMix) {
4969
6247
  throw new Error("Traffic mix scenario shares must be configured before registration.");
4970
6248
  }
4971
6249
  const weights = scenarioMix.map((share) => share.weight);
6250
+ const seedId = (0, load_engine_v2_js_1.buildLoadEngineV2TrafficMixSeedId)(declarationIndex, trafficMix.name);
4972
6251
  return scenarioMix.map((share, index) => {
4973
- const splitSimulations = totalLoad
4974
- .map((simulation) => splitTrafficSimulation(simulation, weights, index))
4975
- .filter((simulation) => simulation != null);
4976
- const scenario = !splitSimulations.length
4977
- ? share.scenario.withLoadSimulations(LoadStrikeSimulation.pause(0))
4978
- : share.scenario.withLoadSimulations(...splitSimulations);
4979
- return scenario.__loadStrikeWithInternalLicenseFeatures(TRAFFIC_MIX_FEATURE);
6252
+ const splitSimulations = totalLoad.map((simulation) => splitTrafficSimulation(simulation, weights, index)
6253
+ ?? trafficMixNoWorkSimulation(simulation));
6254
+ return share.scenario
6255
+ .withLoadSimulations(...splitSimulations)
6256
+ .__loadStrikeWithInternalLicenseFeatures(TRAFFIC_MIX_FEATURE)
6257
+ .__loadStrikeSetTrafficMixV2Metadata({
6258
+ declarationIndex,
6259
+ name: trafficMix.name,
6260
+ laneIndex: index,
6261
+ shareWeight: share.weight,
6262
+ shareWeights: weights,
6263
+ seedId,
6264
+ originalSimulations: totalLoad
6265
+ });
4980
6266
  });
4981
6267
  }
6268
+ function trafficMixNoWorkSimulation(simulation) {
6269
+ const kind = String(simulation.Kind ?? "");
6270
+ return kind === "IterationsForInject" || kind === "IterationsForConstant"
6271
+ ? LoadStrikeSimulation.pause(0)
6272
+ : LoadStrikeSimulation.pause(Math.max(readFiniteSimulationNumber(simulation, "DuringSeconds"), 0));
6273
+ }
4982
6274
  function splitTrafficSimulation(simulation, weights, index) {
4983
6275
  const kind = String(simulation.Kind ?? "");
4984
6276
  const duringSeconds = readFiniteSimulationNumber(simulation, "DuringSeconds");
@@ -5065,6 +6357,8 @@ function attachScenarioStatsAliases(scenario) {
5065
6357
  const normalized = normalizeScenarioStatsValue(scenario);
5066
6358
  normalized.ok = attachMeasurementStatsAliases(normalized.ok);
5067
6359
  normalized.fail = attachMeasurementStatsAliases(normalized.fail);
6360
+ if (normalized.allMeasurement)
6361
+ normalized.allMeasurement = attachMeasurementStatsAliases(normalized.allMeasurement);
5068
6362
  normalized.loadSimulationStats = attachLoadSimulationStatsAliases(normalized.loadSimulationStats);
5069
6363
  normalized.stepStats = normalized.stepStats.map((value) => attachStepStatsAliases(value));
5070
6364
  const findStepStats = scenario.findStepStats ?? ((stepName) => normalized.stepStats.find((value) => value.stepName === stepName));
@@ -5098,6 +6392,7 @@ function attachScenarioStatsAliases(scenario) {
5098
6392
  DurationMs: "durationMs",
5099
6393
  Ok: "ok",
5100
6394
  Fail: "fail",
6395
+ AllMeasurement: "allMeasurement",
5101
6396
  LoadSimulationStats: "loadSimulationStats",
5102
6397
  SortIndex: "sortIndex",
5103
6398
  StepStats: "stepStats"
@@ -5118,6 +6413,10 @@ function attachNodeStatsAliases(stats) {
5118
6413
  : stats.scenarioStats.flatMap((value) => value.stepStats)).map((value) => attachStepStatsAliases(value));
5119
6414
  stats.pluginsData = stats.pluginsData.map((value) => normalizePluginData(value.pluginName ?? value.PluginName ?? "", value));
5120
6415
  stats.sinkErrors = stats.sinkErrors.map((value) => attachSinkErrorAliases(value));
6416
+ stats.generatorWarnings = (stats.generatorWarnings ?? []).map(attachGeneratorWarningAliases);
6417
+ stats.schedulerSegments = (stats.schedulerSegments ?? []).map((value) => normalizeSchedulerSegment(value));
6418
+ stats.observationDeliveryStats = normalizeObservationDeliveryStats(stats.observationDeliveryStats ?? emptyObservationDeliveryStats());
6419
+ stats.reportingComplete ?? (stats.reportingComplete = false);
5121
6420
  const findScenarioStats = stats.findScenarioStats ?? ((scenarioName) => stats.scenarioStats.find((value) => value.scenarioName === scenarioName));
5122
6421
  const getScenarioStats = stats.getScenarioStats ?? ((scenarioName) => {
5123
6422
  const value = findScenarioStats(scenarioName);
@@ -5152,7 +6451,11 @@ function attachNodeStatsAliases(stats) {
5152
6451
  DisabledSinks: "disabledSinks",
5153
6452
  SinkErrors: "sinkErrors",
5154
6453
  ReportFiles: "reportFiles",
5155
- LogFiles: "logFiles"
6454
+ LogFiles: "logFiles",
6455
+ GeneratorWarnings: "generatorWarnings",
6456
+ SchedulerSegments: "schedulerSegments",
6457
+ ObservationDeliveryStats: "observationDeliveryStats",
6458
+ ReportingComplete: "reportingComplete"
5156
6459
  });
5157
6460
  defineAliasProperty(projected, "StartedUtc", () => parseAliasDate(stats.startedUtc));
5158
6461
  defineAliasProperty(projected, "CompletedUtc", () => parseAliasDate(stats.completedUtc));
@@ -5208,11 +6511,43 @@ function attachRunResultAliases(result) {
5208
6511
  .map((value) => ({ ...asAliasRecord(value) })),
5209
6512
  failedCorrelationRows: pickAliasArray(source, "failedCorrelationRows", "FailedCorrelationRows")
5210
6513
  .map((value) => ({ ...asAliasRecord(value) })),
6514
+ ...(hasAliasValue(source, "generatorWarnings", "GeneratorWarnings")
6515
+ ? {
6516
+ generatorWarnings: pickAliasArray(source, "generatorWarnings", "GeneratorWarnings")
6517
+ .map(attachGeneratorWarningAliases)
6518
+ }
6519
+ : {}),
6520
+ ...(hasAliasValue(source, "schedulerSegments", "SchedulerSegments")
6521
+ ? {
6522
+ schedulerSegments: pickAliasArray(source, "schedulerSegments", "SchedulerSegments")
6523
+ .map(normalizeSchedulerSegment)
6524
+ }
6525
+ : {}),
6526
+ ...(hasAliasValue(source, "schedulerStats", "SchedulerStats")
6527
+ ? { schedulerStats: normalizeSchedulerStats(pickAliasValue(source, "schedulerStats", "SchedulerStats")) }
6528
+ : {}),
6529
+ ...(hasAliasValue(source, "observationDeliveryStats", "ObservationDeliveryStats")
6530
+ ? {
6531
+ observationDeliveryStats: normalizeObservationDeliveryStats(pickAliasValue(source, "observationDeliveryStats", "ObservationDeliveryStats"))
6532
+ }
6533
+ : {}),
6534
+ ...(hasAliasValue(source, "reportingComplete", "ReportingComplete")
6535
+ ? { reportingComplete: pickAliasBoolean(source, "reportingComplete", "ReportingComplete") }
6536
+ : {}),
5211
6537
  findScenarioStats,
5212
6538
  getScenarioStats,
5213
6539
  FindScenarioStats: findScenarioStats,
5214
6540
  GetScenarioStats: getScenarioStats
5215
6541
  };
6542
+ const schedulerDistributions = result[LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS];
6543
+ if (schedulerDistributions) {
6544
+ Object.defineProperty(projected, LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS, {
6545
+ value: schedulerDistributions.map(cloneLoadEngineV2DistributionRecord),
6546
+ enumerable: false,
6547
+ configurable: false,
6548
+ writable: false
6549
+ });
6550
+ }
5216
6551
  attachAliasMap(projected, {
5217
6552
  AllBytes: "allBytes",
5218
6553
  AllRequestCount: "allRequestCount",
@@ -5236,7 +6571,12 @@ function attachRunResultAliases(result) {
5236
6571
  ReportFiles: "reportFiles",
5237
6572
  LogFiles: "logFiles",
5238
6573
  CorrelationRows: "correlationRows",
5239
- FailedCorrelationRows: "failedCorrelationRows"
6574
+ FailedCorrelationRows: "failedCorrelationRows",
6575
+ GeneratorWarnings: "generatorWarnings",
6576
+ SchedulerSegments: "schedulerSegments",
6577
+ SchedulerStats: "schedulerStats",
6578
+ ObservationDeliveryStats: "observationDeliveryStats",
6579
+ ReportingComplete: "reportingComplete"
5240
6580
  });
5241
6581
  defineAliasProperty(projected, "StartedUtc", () => parseAliasDate(projected.startedUtc));
5242
6582
  defineAliasProperty(projected, "CompletedUtc", () => parseAliasDate(projected.completedUtc));
@@ -5844,6 +7184,14 @@ function detailedToNodeStats(result, metricStats) {
5844
7184
  sinkErrors: (result.sinkErrors ?? []).map((sinkError) => ({ ...sinkError })),
5845
7185
  reportFiles: [...(result.reportFiles ?? [])],
5846
7186
  logFiles: [...(result.logFiles ?? [])],
7187
+ generatorWarnings: (result.generatorWarnings ?? []).map(attachGeneratorWarningAliases),
7188
+ schedulerSegments: result.schedulerSegments?.map((segment) => ({ ...segment })),
7189
+ schedulerStats: result.schedulerStats
7190
+ ? normalizeSchedulerStats(result.schedulerStats)
7191
+ : undefined,
7192
+ [LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS]: result[LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS]?.map(cloneLoadEngineV2DistributionRecord),
7193
+ observationDeliveryStats: normalizeObservationDeliveryStats(result.observationDeliveryStats ?? emptyObservationDeliveryStats()),
7194
+ reportingComplete: result.reportingComplete ?? true,
5847
7195
  findScenarioStats: (scenarioName) => scenarioStats.find((scenario) => scenario.scenarioName === scenarioName),
5848
7196
  getScenarioStats: (scenarioName) => {
5849
7197
  const value = scenarioStats.find((scenario) => scenario.scenarioName === scenarioName);
@@ -5911,22 +7259,35 @@ function buildEmptyNodeStats(args) {
5911
7259
  sinkErrors: [],
5912
7260
  reportFiles: [],
5913
7261
  logFiles: [],
7262
+ generatorWarnings: [],
7263
+ observationDeliveryStats: emptyObservationDeliveryStats(),
7264
+ reportingComplete: true,
5914
7265
  findScenarioStats: (scenarioName) => undefined,
5915
7266
  getScenarioStats: (scenarioName) => {
5916
7267
  throw new Error(`Scenario stats not found: ${scenarioName}`);
5917
7268
  }
5918
7269
  });
5919
7270
  }
5920
- function planRuntimeClusterAssignments(scenarios, agentsCount, targetScenarios, agentTargetScenarios) {
7271
+ function planRuntimeClusterAssignments(scenarios, agentsCount, targetScenarios, agentTargetScenarios, globalV2 = false, coordinatorTargetScenarios = []) {
5921
7272
  const resolvedAgentCount = Math.max(agentsCount, 1);
5922
- const selectedScenarios = targetScenarios.length
7273
+ let selectedScenarios = targetScenarios.length
5923
7274
  ? scenarios.filter((scenario) => targetScenarios.includes(scenario.name))
5924
7275
  : [...scenarios];
7276
+ if (globalV2 && coordinatorTargetScenarios.length) {
7277
+ const coordinatorNames = new Set(coordinatorTargetScenarios);
7278
+ selectedScenarios = selectedScenarios.filter((scenario) => !coordinatorNames.has(scenario.name));
7279
+ }
5925
7280
  if (!selectedScenarios.length) {
5926
7281
  return Array.from({ length: resolvedAgentCount }, () => []);
5927
7282
  }
5928
7283
  if (agentTargetScenarios.length) {
5929
- return Array.from({ length: resolvedAgentCount }, () => [...agentTargetScenarios]);
7284
+ const coordinatorNames = new Set(coordinatorTargetScenarios);
7285
+ const names = agentTargetScenarios.filter((name) => !coordinatorNames.has(name));
7286
+ return Array.from({ length: resolvedAgentCount }, () => [...names]);
7287
+ }
7288
+ if (globalV2) {
7289
+ const scenarioNames = selectedScenarios.map((scenario) => scenario.name);
7290
+ return Array.from({ length: resolvedAgentCount }, () => [...scenarioNames]);
5930
7291
  }
5931
7292
  const weightedNames = [];
5932
7293
  for (const scenario of selectedScenarios) {
@@ -5946,7 +7307,320 @@ function planRuntimeClusterAssignments(scenarios, agentsCount, targetScenarios,
5946
7307
  }
5947
7308
  return assignments.map((entry) => [...entry]);
5948
7309
  }
5949
- function nodeStatsToClusterPayload(result) {
7310
+ function buildRuntimeLoadEngineV2Plan(scenarios, options, testInfo, expectedAgentIds) {
7311
+ validateLoadEngineV2ScenarioFeatures(scenarios);
7312
+ const kindToken = (value) => {
7313
+ const normalized = String(value ?? "").replace(/[^a-z0-9]/gi, "").toLowerCase();
7314
+ const tokens = {
7315
+ inject: "inject", injectrandom: "inject-random", rampinginject: "ramping-inject",
7316
+ keepconstant: "keep-constant", rampingconstant: "ramping-constant",
7317
+ iterationsforinject: "iterations-for-inject", iterationsforconstant: "iterations-for-constant",
7318
+ pause: "pause"
7319
+ };
7320
+ const token = tokens[normalized];
7321
+ if (!token)
7322
+ throw new Error(`Load Engine V2 simulation kind is unsupported: ${String(value ?? "")}`);
7323
+ return token;
7324
+ };
7325
+ const integer = (value) => Math.max(Math.trunc(toNumber(value)), 0).toString();
7326
+ const ns = (value) => Math.max(Math.round(toNumber(value) * 1000000000), 0).toString();
7327
+ const coordinatorNames = new Set(options.coordinatorTargetScenarios ?? []);
7328
+ const explicitAgentNames = new Set(options.agentTargetScenarios ?? []);
7329
+ const selectedNames = new Set(options.targetScenarios ?? []);
7330
+ const plannedScenarios = scenarios
7331
+ .map((scenario, declarationIndex) => ({ scenario, declarationIndex }))
7332
+ .filter(({ scenario }) => (selectedNames.size === 0 || selectedNames.has(scenario.name))
7333
+ && (explicitAgentNames.size > 0
7334
+ ? explicitAgentNames.has(scenario.name)
7335
+ : !coordinatorNames.has(scenario.name)));
7336
+ const implicitSingleInvocation = {
7337
+ simulationIndex64: "0",
7338
+ kind: "iterations-for-constant",
7339
+ rate64: "0",
7340
+ minRate64: "0",
7341
+ maxRate64: "0",
7342
+ copies64: "1",
7343
+ iterations64: "1",
7344
+ intervalNs64: "0",
7345
+ durationNs64: "0"
7346
+ };
7347
+ return {
7348
+ runId: testInfo.sessionId,
7349
+ sessionId: testInfo.sessionId,
7350
+ registrationNonce: (0, node_crypto_1.randomBytes)(16).toString("hex"),
7351
+ maxInFlight64: Math.max(options.maxInFlight ?? 10000, 1).toString(),
7352
+ reportingIntervalNs64: ns(options.reportingIntervalSeconds ?? 1),
7353
+ schedulerVisibleProcessorCount64: Math.max(node_os_1.default.cpus().length, 1).toString(),
7354
+ expectedAgentIds: [...expectedAgentIds],
7355
+ scenarios: plannedScenarios.map(({ scenario, declarationIndex }) => ({
7356
+ scenarioIndex64: declarationIndex.toString(),
7357
+ scenarioName: scenario.name,
7358
+ target: "agent",
7359
+ callbackExecutionMode: "async",
7360
+ declaredStepNames: scenario.getDeclaredSteps(),
7361
+ simulations: scenario.getSimulations().length
7362
+ ? scenario.getSimulations().map((simulation, simulationIndex) => ({
7363
+ simulationIndex64: simulationIndex.toString(),
7364
+ kind: kindToken(simulation.Kind ?? simulation.kind),
7365
+ rate64: integer(simulation.Rate ?? simulation.rate),
7366
+ minRate64: integer(simulation.MinRate ?? simulation.minRate),
7367
+ maxRate64: integer(simulation.MaxRate ?? simulation.maxRate),
7368
+ copies64: integer(simulation.Copies ?? simulation.copies),
7369
+ iterations64: integer(simulation.Iterations ?? simulation.iterations),
7370
+ intervalNs64: ns(simulation.IntervalSeconds ?? simulation.intervalSeconds),
7371
+ durationNs64: ns(simulation.DuringSeconds ?? simulation.duringSeconds)
7372
+ }))
7373
+ : [{ ...implicitSingleInvocation }]
7374
+ }))
7375
+ };
7376
+ }
7377
+ function normalizeRequiredV2AgentIds(values, expectedCount) {
7378
+ const ids = normalizeOptionalStringArray(values) ?? [];
7379
+ if (ids.length !== expectedCount || new Set(ids).size !== ids.length) {
7380
+ throw new Error("Remote Load Engine V2 coordinators require an exact unique ExpectedAgentIds set matching AgentsCount.");
7381
+ }
7382
+ return ids;
7383
+ }
7384
+ function validateLoadEngineV2ScenarioFeatures(scenarios) {
7385
+ for (const scenario of scenarios) {
7386
+ if (scenario.getTrackingConfiguration()) {
7387
+ throw new Error(`Load Engine V2 correlation is not available in the supported non-correlation profile. Scenario=${scenario.name}.`);
7388
+ }
7389
+ }
7390
+ }
7391
+ function canonicalLoadEngineV2InvocationIdentityKind(simulationKind) {
7392
+ const normalized = simulationKind.replace(/[^a-z0-9]/gi, "").toLowerCase();
7393
+ if (["inject", "injectrandom", "rampinginject", "iterationsforinject"].includes(normalized)) {
7394
+ return "arrival";
7395
+ }
7396
+ if (["keepconstant", "rampingconstant"].includes(normalized)) {
7397
+ return "worker-iteration";
7398
+ }
7399
+ if (normalized === "iterationsforconstant") {
7400
+ return "constant-iteration";
7401
+ }
7402
+ return "";
7403
+ }
7404
+ function buildRuntimeLoadEngineV2HistogramArtifact(result, scenarioDeclarations) {
7405
+ const emptyHistogram = () => new load_engine_v2_js_1.LoadStrikeHistogramV1().toSidecar();
7406
+ const distributions = [];
7407
+ const measurementSummaries = [];
7408
+ const statusBody = (measurement, outcome) => {
7409
+ const named = measurement.statusCodes
7410
+ .filter((row) => Boolean(row.statusCode || row.message))
7411
+ .map((row) => {
7412
+ const key = (0, cluster_js_1.buildLoadEngineV2StatusIdentityKey)(row.statusCode, row.message);
7413
+ if (!key)
7414
+ throw new Error("Load Engine V2 status identity unexpectedly resolved empty.");
7415
+ return {
7416
+ statusIdentityKeyHex: key.identity.toString("hex"),
7417
+ display: key.display,
7418
+ count64: Math.max(Math.trunc(row.count), 0).toString(),
7419
+ aggregatedObservationCount64: "0",
7420
+ hasAggregatedIdentities: false
7421
+ };
7422
+ })
7423
+ .sort((left, right) => Buffer.compare(Buffer.from(left.statusIdentityKeyHex, "hex"), Buffer.from(right.statusIdentityKeyHex, "hex")));
7424
+ let statuses = named;
7425
+ if (named.length > 64) {
7426
+ const retained = named.slice(0, 63);
7427
+ const aggregated = named.slice(63).reduce((sum, row) => sum + BigInt(row.count64), 0n);
7428
+ retained.push({
7429
+ statusIdentityKeyHex: Buffer.concat([Buffer.from("LS-ID1\n", "ascii"), Buffer.from([0x0d])]).toString("hex"),
7430
+ display: "<other>", count64: aggregated.toString(),
7431
+ aggregatedObservationCount64: aggregated.toString(), hasAggregatedIdentities: aggregated > 0n
7432
+ });
7433
+ statuses = retained;
7434
+ }
7435
+ return {
7436
+ outcome,
7437
+ statusObservationCount64: statuses.reduce((sum, row) => sum + BigInt(row.count64), 0n).toString(),
7438
+ statuses
7439
+ };
7440
+ };
7441
+ const appendMeasurement = (seriesKind, scenarioIndex64, scenarioName, identity, display, ok, fail, all, reservedOther) => {
7442
+ const emptyMeasurement = () => ({
7443
+ count64: "0",
7444
+ histogramSidecar: {
7445
+ latency: emptyHistogram(), size: emptyHistogram(), allBytes64: "0",
7446
+ lessOrEq80064: "0", more800Less120064: "0", moreOrEq120064: "0"
7447
+ },
7448
+ request: { count: 0, percent: 0, rps: 0 },
7449
+ dataTransfer: { allBytes: 0, minBytes: 0, maxBytes: 0, meanBytes: 0, percent50: 0,
7450
+ percent75: 0, percent95: 0, percent99: 0, percent100: 0, stdDev: 0 },
7451
+ latency: { latencyCount: { lessOrEq800: 0, more800Less1200: 0, moreOrEq1200: 0 },
7452
+ minMs: 0, maxMs: 0, meanMs: 0, percent50: 0, percent75: 0, percent95: 0,
7453
+ percent99: 0, stdDev: 0 },
7454
+ statusCodes: []
7455
+ });
7456
+ const okValue = ok ?? emptyMeasurement();
7457
+ const failValue = fail ?? emptyMeasurement();
7458
+ const allValue = all ?? emptyMeasurement();
7459
+ for (const [outcome, measurement] of [
7460
+ ["ok", okValue], ["fail", failValue], ["all", allValue]
7461
+ ]) {
7462
+ const sidecar = measurement.histogramSidecar;
7463
+ if (!sidecar)
7464
+ throw new Error("Load Engine V2 assigned measurement is missing histogram state.");
7465
+ distributions.push({
7466
+ seriesKind, scenarioIndex64, scenarioName, identityKeyHex: identity.toString("hex"),
7467
+ outcome, unit: "microseconds", histogram: sidecar.latency,
7468
+ exactTotalDecimalOrEmpty: sidecar.latency.exactTotal64,
7469
+ bands: [
7470
+ { bandId64: "0", count64: sidecar.lessOrEq80064 },
7471
+ { bandId64: "1", count64: sidecar.more800Less120064 },
7472
+ { bandId64: "2", count64: sidecar.moreOrEq120064 }
7473
+ ]
7474
+ });
7475
+ distributions.push({
7476
+ seriesKind, scenarioIndex64, scenarioName, identityKeyHex: identity.toString("hex"),
7477
+ outcome, unit: "bytes", histogram: sidecar.size,
7478
+ exactTotalDecimalOrEmpty: sidecar.size.exactTotal64
7479
+ });
7480
+ }
7481
+ const observation = BigInt(allValue.count64 ?? allValue.histogramSidecar?.latency.count64 ?? "0");
7482
+ const success = BigInt(okValue.count64 ?? okValue.histogramSidecar?.latency.count64 ?? "0");
7483
+ const failure = BigInt(failValue.count64 ?? failValue.histogramSidecar?.latency.count64 ?? "0");
7484
+ measurementSummaries.push({
7485
+ seriesKind, scenarioIndex64, scenarioName, identityKeyHex: identity.toString("hex"), display,
7486
+ observationCount64: observation.toString(), successCount64: success.toString(),
7487
+ failureCount64: failure.toString(),
7488
+ aggregatedObservationCount64: reservedOther ? observation.toString() : "0",
7489
+ hasAggregatedIdentities: reservedOther && observation > 0n,
7490
+ outcomes: [statusBody(okValue, "ok"), statusBody(failValue, "fail")]
7491
+ });
7492
+ };
7493
+ for (const scenario of result.scenarioStats) {
7494
+ const scenarioIndex64 = Math.max(Math.trunc(scenario.sortIndex), 0).toString();
7495
+ appendMeasurement("scenario", scenarioIndex64, scenario.scenarioName, (0, cluster_js_1.buildLoadEngineV2ScenarioIdentityKey)(scenarioIndex64), scenario.scenarioName, scenario.ok, scenario.fail, scenario.allMeasurement, false);
7496
+ const declaration = scenarioDeclarations?.find((value) => value.name === scenario.scenarioName);
7497
+ const declaredNames = declaration?.getDeclaredSteps()
7498
+ ?? (scenarioDeclarations ? [] : undefined);
7499
+ if (declaredNames === undefined) {
7500
+ for (const step of scenario.stepStats) {
7501
+ const key = (0, cluster_js_1.buildLoadEngineV2StepIdentityKey)(scenarioIndex64, step.stepName);
7502
+ appendMeasurement("step", scenarioIndex64, scenario.scenarioName, key.identity, key.display, step.ok, step.fail, step.allMeasurement, false);
7503
+ }
7504
+ }
7505
+ else {
7506
+ const requiredStepAllMeasurement = (step) => {
7507
+ if (!step.allMeasurement) {
7508
+ throw new Error("Load Engine V2 assigned step is missing its all-outcome histogram state.");
7509
+ }
7510
+ return step.allMeasurement;
7511
+ };
7512
+ const groups = new Map();
7513
+ for (const declaredName of declaredNames) {
7514
+ const key = (0, cluster_js_1.buildLoadEngineV2StepIdentityKey)(scenarioIndex64, declaredName);
7515
+ groups.set(key.identity.toString("hex"), {
7516
+ identity: key.identity, display: key.display, routedToOther: false, steps: []
7517
+ });
7518
+ }
7519
+ for (const step of scenario.stepStats) {
7520
+ const resolved = (0, cluster_js_1.resolveLoadEngineV2StepIdentityKey)(scenarioIndex64, step.stepName, declaredNames);
7521
+ const key = resolved.identity.toString("hex");
7522
+ const group = groups.get(key) ?? {
7523
+ identity: resolved.identity,
7524
+ display: resolved.display,
7525
+ routedToOther: resolved.routedToOther,
7526
+ steps: []
7527
+ };
7528
+ group.steps.push(step);
7529
+ groups.set(key, group);
7530
+ }
7531
+ const reservedHex = (0, cluster_js_1.buildLoadEngineV2ReservedStepOtherIdentityKey)(scenarioIndex64).toString("hex");
7532
+ const declaredGroups = [...groups.entries()]
7533
+ .filter(([key]) => key !== reservedHex)
7534
+ .map(([, value]) => value)
7535
+ .sort((left, right) => Buffer.compare(left.identity, right.identity));
7536
+ for (const group of declaredGroups) {
7537
+ const ok = group.steps.length
7538
+ ? aggregateMeasurementStats(group.steps.map((step) => step.ok), scenario.allRequestCount, scenario.durationMs, true)
7539
+ : undefined;
7540
+ const fail = group.steps.length
7541
+ ? aggregateMeasurementStats(group.steps.map((step) => step.fail), scenario.allRequestCount, scenario.durationMs, true)
7542
+ : undefined;
7543
+ const all = group.steps.length
7544
+ ? aggregateMeasurementStats(group.steps.map(requiredStepAllMeasurement), scenario.allRequestCount, scenario.durationMs, true)
7545
+ : undefined;
7546
+ appendMeasurement("step", scenarioIndex64, scenario.scenarioName, group.identity, group.display, ok, fail, all, false);
7547
+ }
7548
+ const other = groups.get(reservedHex);
7549
+ if (other?.steps.length) {
7550
+ appendMeasurement("step", scenarioIndex64, scenario.scenarioName, other.identity, "<other>", aggregateMeasurementStats(other.steps.map((step) => step.ok), scenario.allRequestCount, scenario.durationMs, true), aggregateMeasurementStats(other.steps.map((step) => step.fail), scenario.allRequestCount, scenario.durationMs, true), aggregateMeasurementStats(other.steps.map(requiredStepAllMeasurement), scenario.allRequestCount, scenario.durationMs, true), true);
7551
+ continue;
7552
+ }
7553
+ }
7554
+ appendMeasurement("step", scenarioIndex64, scenario.scenarioName, (0, cluster_js_1.buildLoadEngineV2ReservedStepOtherIdentityKey)(scenarioIndex64), "<other>", undefined, undefined, undefined, true);
7555
+ }
7556
+ for (const segment of result.schedulerSegments ?? []) {
7557
+ const scenarioIndex64 = segment.scenarioIndex.toString();
7558
+ const simulationIndex64 = segment.simulationIndex.toString();
7559
+ for (const kind of ["decision", "start"]) {
7560
+ const seriesKind = kind === "decision" ? "scheduler-decision-lag" : "scheduler-start-lag";
7561
+ const identityKeyHex = (0, cluster_js_1.buildLoadEngineV2SchedulerIdentityKey)(kind, scenarioIndex64, simulationIndex64).toString("hex");
7562
+ const signed = result[LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS]?.find((record) => record.seriesKind === seriesKind
7563
+ && record.scenarioIndex64 === scenarioIndex64
7564
+ && record.identityKeyHex === identityKeyHex
7565
+ && record.outcome === "none"
7566
+ && record.unit === "microseconds");
7567
+ if (!distributions.some((record) => record.seriesKind === seriesKind
7568
+ && record.scenarioIndex64 === scenarioIndex64
7569
+ && record.identityKeyHex === identityKeyHex)) {
7570
+ distributions.push(signed ? cloneLoadEngineV2DistributionRecord(signed) : {
7571
+ seriesKind,
7572
+ scenarioIndex64, scenarioName: segment.scenarioName,
7573
+ identityKeyHex,
7574
+ outcome: "none", unit: "microseconds", histogram: emptyHistogram(),
7575
+ exactTotalDecimalOrEmpty: "0"
7576
+ });
7577
+ }
7578
+ }
7579
+ }
7580
+ return (0, load_engine_v2_js_1.serializeLoadEngineV2HistogramArtifact)({ distributions, measurementSummaries });
7581
+ }
7582
+ function cloneLoadEngineV2DistributionRecord(record) {
7583
+ return {
7584
+ ...record,
7585
+ histogram: {
7586
+ ...record.histogram,
7587
+ exactSamples64: [...record.histogram.exactSamples64],
7588
+ buckets: record.histogram.buckets.map((bucket) => ({ ...bucket }))
7589
+ },
7590
+ bands: record.bands?.map((band) => ({ ...band }))
7591
+ };
7592
+ }
7593
+ function mergeLoadEngineV2SchedulerDistributions(records) {
7594
+ const merged = new Map();
7595
+ for (const input of records) {
7596
+ if (input.seriesKind !== "scheduler-decision-lag" && input.seriesKind !== "scheduler-start-lag") {
7597
+ continue;
7598
+ }
7599
+ const key = [input.seriesKind, input.scenarioIndex64, input.identityKeyHex, input.outcome, input.unit].join("\0");
7600
+ const current = merged.get(key);
7601
+ if (!current) {
7602
+ merged.set(key, cloneLoadEngineV2DistributionRecord(input));
7603
+ continue;
7604
+ }
7605
+ if (current.scenarioName !== input.scenarioName) {
7606
+ throw new Error("Load Engine V2 scheduler histogram identities disagree across agents.");
7607
+ }
7608
+ const histogram = load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(current.histogram);
7609
+ histogram.merge(load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(input.histogram));
7610
+ current.histogram = histogram.toSidecar();
7611
+ current.exactTotalDecimalOrEmpty = histogram.toSidecar().exactTotal64;
7612
+ const bands = new Map();
7613
+ for (const band of [...(current.bands ?? []), ...(input.bands ?? [])]) {
7614
+ bands.set(band.bandId64, (bands.get(band.bandId64) ?? 0n) + BigInt(band.count64));
7615
+ }
7616
+ current.bands = Array.from(bands, ([bandId64, count]) => ({ bandId64, count64: count.toString() }));
7617
+ }
7618
+ return Array.from(merged.values());
7619
+ }
7620
+ function nodeStatsToClusterPayload(result, scenarioDeclarations, requireLoadEngineV2Histogram = false) {
7621
+ const histogramArtifactBase64 = requireLoadEngineV2Histogram
7622
+ ? buildRuntimeLoadEngineV2HistogramArtifact(result, scenarioDeclarations).toString("base64")
7623
+ : undefined;
5950
7624
  return {
5951
7625
  allBytes: result.allBytes,
5952
7626
  allRequestCount: result.allRequestCount,
@@ -5959,7 +7633,13 @@ function nodeStatsToClusterPayload(result) {
5959
7633
  pluginsData: result.pluginsData,
5960
7634
  nodeInfo: result.nodeInfo,
5961
7635
  testInfo: result.testInfo,
5962
- logFiles: [...(result.logFiles ?? [])]
7636
+ logFiles: [...(result.logFiles ?? [])],
7637
+ generatorWarnings: result.generatorWarnings,
7638
+ observationDeliveryStats: result.observationDeliveryStats,
7639
+ schedulerSegments: result.schedulerSegments,
7640
+ schedulerStats: result.schedulerStats,
7641
+ ...(histogramArtifactBase64 ? { histogramArtifactBase64 } : {}),
7642
+ reportingComplete: result.reportingComplete
5963
7643
  };
5964
7644
  }
5965
7645
  function toDetailedRunResultFromNodeStats(result, startedUtc, sinkErrors, policyErrors = []) {
@@ -5987,6 +7667,14 @@ function toDetailedRunResultFromNodeStats(result, startedUtc, sinkErrors, policy
5987
7667
  policyErrors: policyErrors.map((value) => attachRuntimePolicyErrorAliases({ ...value })),
5988
7668
  reportFiles: [...result.reportFiles],
5989
7669
  logFiles: [...(result.logFiles ?? [])],
7670
+ generatorWarnings: (result.generatorWarnings ?? []).map(attachGeneratorWarningAliases),
7671
+ schedulerSegments: result.schedulerSegments?.map((segment) => ({ ...segment })),
7672
+ schedulerStats: result.schedulerStats
7673
+ ? normalizeSchedulerStats(result.schedulerStats)
7674
+ : undefined,
7675
+ [LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS]: result[LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS]?.map(cloneLoadEngineV2DistributionRecord),
7676
+ observationDeliveryStats: normalizeObservationDeliveryStats(result.observationDeliveryStats ?? emptyObservationDeliveryStats()),
7677
+ reportingComplete: result.reportingComplete ?? false,
5990
7678
  correlationRows: buildDetailedCorrelationRows(),
5991
7679
  failedCorrelationRows: buildDetailedFailedCorrelationRows()
5992
7680
  });
@@ -6009,7 +7697,198 @@ function flattenMetricValues(metricStats) {
6009
7697
  }))
6010
7698
  ];
6011
7699
  }
6012
- function clusterNodeResultToNodeStats(result, testInfo, fallbackNodeInfo) {
7700
+ function projectLoadEngineV2MeasurementsFromArtifact(sourceScenarios, artifact) {
7701
+ const distributions = new Map();
7702
+ for (const distribution of artifact.distributions) {
7703
+ const key = loadEngineV2DistributionProjectionKey(distribution.seriesKind, distribution.scenarioIndex64, distribution.identityKeyHex, distribution.outcome, distribution.unit);
7704
+ if (distributions.has(key)) {
7705
+ throw new Error("Load Engine V2 histogram artifact contains a duplicate distribution identity.");
7706
+ }
7707
+ distributions.set(key, distribution);
7708
+ }
7709
+ const scenarioSummaries = artifact.measurementSummaries
7710
+ .filter((summary) => summary.seriesKind === "scenario")
7711
+ .sort((left, right) => compareLoadEngineV2Decimal(left.scenarioIndex64, right.scenarioIndex64));
7712
+ if (scenarioSummaries.length !== sourceScenarios.length) {
7713
+ throw new Error("Load Engine V2 histogram artifact scenario summaries do not reconcile with the scheduler snapshot.");
7714
+ }
7715
+ const sourceByName = new Map();
7716
+ for (const scenario of sourceScenarios) {
7717
+ if (!scenario.scenarioName || sourceByName.has(scenario.scenarioName)) {
7718
+ throw new Error("Load Engine V2 scheduler snapshot scenario identities are empty or duplicated.");
7719
+ }
7720
+ sourceByName.set(scenario.scenarioName, scenario);
7721
+ }
7722
+ return scenarioSummaries.map((summary) => {
7723
+ const source = sourceByName.get(summary.scenarioName);
7724
+ if (!source) {
7725
+ throw new Error("Load Engine V2 histogram artifact scenario identity is absent from the scheduler snapshot.");
7726
+ }
7727
+ const expectedIdentity = (0, cluster_js_1.buildLoadEngineV2ScenarioIdentityKey)(summary.scenarioIndex64).toString("hex");
7728
+ if (summary.identityKeyHex !== expectedIdentity) {
7729
+ throw new Error("Load Engine V2 histogram artifact scenario identity is not canonical.");
7730
+ }
7731
+ const observationCount = loadEngineV2SafeNumber(summary.observationCount64, "scenario observation count");
7732
+ const successCount = loadEngineV2SafeNumber(summary.successCount64, "scenario success count");
7733
+ const failureCount = loadEngineV2SafeNumber(summary.failureCount64, "scenario failure count");
7734
+ if (source.allRequestCount !== observationCount
7735
+ || source.allOkCount !== successCount
7736
+ || source.allFailCount !== failureCount) {
7737
+ throw new Error("Load Engine V2 histogram artifact scenario counts do not reconcile with the scheduler snapshot.");
7738
+ }
7739
+ const ok = projectLoadEngineV2Measurement(summary, "ok", distributions, observationCount, source.durationMs, [source.ok]);
7740
+ const fail = projectLoadEngineV2Measurement(summary, "fail", distributions, observationCount, source.durationMs, [source.fail]);
7741
+ const allMeasurement = projectLoadEngineV2Measurement(summary, "all", distributions, observationCount, source.durationMs, [source.ok, source.fail]);
7742
+ const stepSummaries = artifact.measurementSummaries
7743
+ .filter((candidate) => candidate.seriesKind === "step"
7744
+ && candidate.scenarioIndex64 === summary.scenarioIndex64
7745
+ && candidate.scenarioName === summary.scenarioName)
7746
+ .sort((left, right) => Buffer.compare(Buffer.from(left.identityKeyHex, "hex"), Buffer.from(right.identityKeyHex, "hex")));
7747
+ const stepIdentitySet = new Set(stepSummaries.map((candidate) => candidate.identityKeyHex));
7748
+ const reservedOtherIdentity = (0, cluster_js_1.buildLoadEngineV2ReservedStepOtherIdentityKey)(summary.scenarioIndex64).toString("hex");
7749
+ const stepStats = stepSummaries.map((stepSummary, sortIndex) => {
7750
+ const matchingSourceSteps = source.stepStats.filter((step) => {
7751
+ const observedIdentity = (0, cluster_js_1.buildLoadEngineV2StepIdentityKey)(summary.scenarioIndex64, step.stepName).identity.toString("hex");
7752
+ return stepSummary.identityKeyHex === reservedOtherIdentity
7753
+ ? !stepIdentitySet.has(observedIdentity)
7754
+ : observedIdentity === stepSummary.identityKeyHex;
7755
+ });
7756
+ const stepObservationCount = loadEngineV2SafeNumber(stepSummary.observationCount64, "step observation count");
7757
+ const stepOk = projectLoadEngineV2Measurement(stepSummary, "ok", distributions, observationCount, source.durationMs, matchingSourceSteps.map((step) => step.ok));
7758
+ const stepFail = projectLoadEngineV2Measurement(stepSummary, "fail", distributions, observationCount, source.durationMs, matchingSourceSteps.map((step) => step.fail));
7759
+ const stepAll = projectLoadEngineV2Measurement(stepSummary, "all", distributions, observationCount, source.durationMs, matchingSourceSteps.flatMap((step) => [step.ok, step.fail]));
7760
+ const totalLatencyMs = stepAll.latency.meanMs * stepObservationCount;
7761
+ return {
7762
+ scenarioName: summary.scenarioName,
7763
+ stepName: stepSummary.display,
7764
+ okCount: stepOk.request.count,
7765
+ failCount: stepFail.request.count,
7766
+ requestCount: stepObservationCount,
7767
+ totalBytes: stepAll.dataTransfer.allBytes,
7768
+ totalLatencyMs,
7769
+ avgLatencyMs: stepObservationCount > 0 ? totalLatencyMs / stepObservationCount : 0,
7770
+ minLatencyMs: stepAll.latency.minMs,
7771
+ maxLatencyMs: stepAll.latency.maxMs,
7772
+ statusCodes: aggregateStatusCodeCounts(stepOk.statusCodes, stepFail.statusCodes),
7773
+ ok: stepOk,
7774
+ fail: stepFail,
7775
+ allMeasurement: stepAll,
7776
+ sortIndex
7777
+ };
7778
+ });
7779
+ const totalLatencyMs = allMeasurement.latency.meanMs * observationCount;
7780
+ const projected = {
7781
+ scenarioName: summary.scenarioName,
7782
+ allRequestCount: observationCount,
7783
+ allOkCount: successCount,
7784
+ allFailCount: failureCount,
7785
+ totalBytes: allMeasurement.dataTransfer.allBytes,
7786
+ totalLatencyMs,
7787
+ avgLatencyMs: observationCount > 0 ? totalLatencyMs / observationCount : 0,
7788
+ minLatencyMs: allMeasurement.latency.minMs,
7789
+ maxLatencyMs: allMeasurement.latency.maxMs,
7790
+ statusCodes: aggregateStatusCodeCounts(ok.statusCodes, fail.statusCodes),
7791
+ allMeasurement,
7792
+ allBytes: allMeasurement.dataTransfer.allBytes,
7793
+ currentOperation: source.currentOperation,
7794
+ durationMs: source.durationMs,
7795
+ ok,
7796
+ fail,
7797
+ loadSimulationStats: { ...source.loadSimulationStats },
7798
+ sortIndex: loadEngineV2SafeNumber(summary.scenarioIndex64, "scenario index"),
7799
+ stepStats,
7800
+ findStepStats: (stepName) => stepStats.find((step) => step.stepName === stepName),
7801
+ getStepStats: (stepName) => {
7802
+ const step = stepStats.find((candidate) => candidate.stepName === stepName);
7803
+ if (!step)
7804
+ throw new Error(`Step stats not found: ${stepName}`);
7805
+ return step;
7806
+ }
7807
+ };
7808
+ return attachScenarioStatsAliases(projected);
7809
+ });
7810
+ }
7811
+ function projectLoadEngineV2Measurement(summary, outcome, distributions, allRequestCount, durationMs, sourceMeasurements) {
7812
+ const expectedCount64 = outcome === "ok"
7813
+ ? summary.successCount64
7814
+ : outcome === "fail"
7815
+ ? summary.failureCount64
7816
+ : summary.observationCount64;
7817
+ const latency = requireLoadEngineV2MeasurementDistribution(summary, outcome, "microseconds", distributions);
7818
+ const size = requireLoadEngineV2MeasurementDistribution(summary, outcome, "bytes", distributions);
7819
+ if (latency.histogram.count64 !== expectedCount64 || size.histogram.count64 !== expectedCount64) {
7820
+ throw new Error("Load Engine V2 histogram distribution counts do not reconcile with its measurement summary.");
7821
+ }
7822
+ const bands = new Map();
7823
+ for (const band of latency.bands ?? []) {
7824
+ if (bands.has(band.bandId64)) {
7825
+ throw new Error("Load Engine V2 latency distribution contains a duplicate band.");
7826
+ }
7827
+ bands.set(band.bandId64, BigInt(band.count64));
7828
+ }
7829
+ if (bands.size !== 3 || !bands.has("0") || !bands.has("1") || !bands.has("2")
7830
+ || [...bands.values()].reduce((sum, value) => sum + value, 0n) !== BigInt(expectedCount64)) {
7831
+ throw new Error("Load Engine V2 latency distribution bands do not reconcile with its count.");
7832
+ }
7833
+ const sourceStatuses = new Map();
7834
+ for (const measurement of sourceMeasurements) {
7835
+ for (const status of measurement.statusCodes) {
7836
+ const identity = (0, cluster_js_1.buildLoadEngineV2StatusIdentityKey)(status.statusCode, status.message);
7837
+ if (identity)
7838
+ sourceStatuses.set(identity.identity.toString("hex"), status);
7839
+ }
7840
+ }
7841
+ const statusCodes = new Map();
7842
+ const outcomeSummaries = outcome === "all"
7843
+ ? summary.outcomes
7844
+ : summary.outcomes.filter((candidate) => candidate.outcome === outcome);
7845
+ for (const outcomeSummary of outcomeSummaries) {
7846
+ for (const status of outcomeSummary.statuses) {
7847
+ const source = sourceStatuses.get(status.statusIdentityKeyHex);
7848
+ statusCodes.set(`${outcomeSummary.outcome}\0${status.statusIdentityKeyHex}`, {
7849
+ statusCode: source?.statusCode ?? status.display,
7850
+ message: source?.message ?? "",
7851
+ isError: source?.isError ?? outcomeSummary.outcome === "fail",
7852
+ count: loadEngineV2SafeNumber(status.count64, "status count")
7853
+ });
7854
+ }
7855
+ }
7856
+ const expectedCount = loadEngineV2SafeNumber(expectedCount64, "measurement count");
7857
+ const sizeHistogram = load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(size.histogram);
7858
+ return buildHistogramMeasurement({
7859
+ count: expectedCount,
7860
+ allBytes: loadEngineV2SafeNumber(sizeHistogram.exactTotal.toString(), "measurement byte total"),
7861
+ latency: load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(latency.histogram),
7862
+ size: sizeHistogram,
7863
+ statusCodes,
7864
+ lessOrEq800: loadEngineV2SafeNumber((bands.get("0") ?? 0n).toString(), "latency band count"),
7865
+ more800Less1200: loadEngineV2SafeNumber((bands.get("1") ?? 0n).toString(), "latency band count"),
7866
+ moreOrEq1200: loadEngineV2SafeNumber((bands.get("2") ?? 0n).toString(), "latency band count")
7867
+ }, allRequestCount, durationMs);
7868
+ }
7869
+ function requireLoadEngineV2MeasurementDistribution(summary, outcome, unit, distributions) {
7870
+ const distribution = distributions.get(loadEngineV2DistributionProjectionKey(summary.seriesKind, summary.scenarioIndex64, summary.identityKeyHex, outcome, unit));
7871
+ if (!distribution || distribution.scenarioName !== summary.scenarioName) {
7872
+ throw new Error("Load Engine V2 measurement summary is missing its canonical histogram distribution.");
7873
+ }
7874
+ return distribution;
7875
+ }
7876
+ function loadEngineV2DistributionProjectionKey(seriesKind, scenarioIndex64, identityKeyHex, outcome, unit) {
7877
+ return [seriesKind, scenarioIndex64, identityKeyHex, outcome, unit].join("\0");
7878
+ }
7879
+ function compareLoadEngineV2Decimal(left, right) {
7880
+ const a = BigInt(left);
7881
+ const b = BigInt(right);
7882
+ return a < b ? -1 : a > b ? 1 : 0;
7883
+ }
7884
+ function loadEngineV2SafeNumber(value, field) {
7885
+ const parsed = BigInt(value);
7886
+ if (parsed < 0n || parsed > BigInt(Number.MAX_SAFE_INTEGER)) {
7887
+ throw new Error(`Load Engine V2 ${field} exceeds the JavaScript safe integer range.`);
7888
+ }
7889
+ return Number(parsed);
7890
+ }
7891
+ function clusterNodeResultToNodeStats(result, testInfo, fallbackNodeInfo, requireHistogramArtifact = false) {
6013
7892
  const completedUtc = new Date().toISOString();
6014
7893
  if (!result.success || !result.stats) {
6015
7894
  return attachNodeStatsAliases({
@@ -6042,12 +7921,12 @@ function clusterNodeResultToNodeStats(result, testInfo, fallbackNodeInfo) {
6042
7921
  isFailed: true,
6043
7922
  errorCount: 1,
6044
7923
  exceptionMessage: result.error ?? "Agent execution failed."
6045
- }]
7924
+ }],
7925
+ reportingComplete: false
6046
7926
  });
6047
7927
  }
6048
7928
  const metrics = normalizeMetricStatsPayload(result.stats.metrics, result.stats.durationMs ?? 0);
6049
- const scenarioStats = normalizeScenarioStatsPayload(result.stats.scenarioStats);
6050
- const stepStats = scenarioStats.flatMap((value) => value.stepStats);
7929
+ let scenarioStats = normalizeScenarioStatsPayload(result.stats.scenarioStats);
6051
7930
  const thresholds = normalizeThresholdPayload(result.stats.thresholds);
6052
7931
  const pluginsData = normalizePluginsPayload(result.stats.pluginsData);
6053
7932
  const nodeInfo = {
@@ -6060,6 +7939,17 @@ function clusterNodeResultToNodeStats(result, testInfo, fallbackNodeInfo) {
6060
7939
  ...testInfo,
6061
7940
  ...(result.stats.testInfo ?? {})
6062
7941
  };
7942
+ const histogramArtifactBase64 = String(result.stats.histogramArtifactBase64 ?? "");
7943
+ if (requireHistogramArtifact && !histogramArtifactBase64) {
7944
+ throw new Error("Load Engine V2 result omits the mandatory LS-H1 histogram artifact.");
7945
+ }
7946
+ const histogramArtifact = histogramArtifactBase64
7947
+ ? (0, load_engine_v2_js_1.parseLoadEngineV2HistogramArtifact)(Buffer.from(histogramArtifactBase64, "base64"))
7948
+ : undefined;
7949
+ if (histogramArtifact) {
7950
+ scenarioStats = projectLoadEngineV2MeasurementsFromArtifact(scenarioStats, histogramArtifact);
7951
+ }
7952
+ const stepStats = scenarioStats.flatMap((value) => value.stepStats);
6063
7953
  return attachNodeStatsAliases({
6064
7954
  startedUtc: testInfo.createdUtc,
6065
7955
  completedUtc,
@@ -6082,6 +7972,17 @@ function clusterNodeResultToNodeStats(result, testInfo, fallbackNodeInfo) {
6082
7972
  sinkErrors: [],
6083
7973
  reportFiles: [],
6084
7974
  logFiles: normalizeAliasStringArray(result.stats.logFiles),
7975
+ generatorWarnings: (result.stats.generatorWarnings ?? []).map(attachGeneratorWarningAliases),
7976
+ schedulerSegments: (result.stats.schedulerSegments ?? []).map(normalizeSchedulerSegment),
7977
+ schedulerStats: result.stats.schedulerStats
7978
+ ? normalizeSchedulerStats(result.stats.schedulerStats)
7979
+ : undefined,
7980
+ [LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS]: histogramArtifact?.distributions
7981
+ .filter((record) => record.seriesKind === "scheduler-decision-lag"
7982
+ || record.seriesKind === "scheduler-start-lag")
7983
+ .map(cloneLoadEngineV2DistributionRecord),
7984
+ observationDeliveryStats: normalizeObservationDeliveryStats(result.stats.observationDeliveryStats ?? emptyObservationDeliveryStats()),
7985
+ reportingComplete: result.stats.reportingComplete ?? false,
6085
7986
  findScenarioStats: (scenarioName) => scenarioStats.find((value) => value.scenarioName === scenarioName),
6086
7987
  getScenarioStats: (scenarioName) => {
6087
7988
  const value = scenarioStats.find((scenario) => scenario.scenarioName === scenarioName);
@@ -6092,7 +7993,46 @@ function clusterNodeResultToNodeStats(result, testInfo, fallbackNodeInfo) {
6092
7993
  }
6093
7994
  });
6094
7995
  }
6095
- function aggregateNodeStats(testInfo, coordinatorNodeInfo, nodes) {
7996
+ function emptyObservationDeliveryStats() {
7997
+ return normalizeObservationDeliveryStats({
7998
+ lastBatchSequence64: "-1",
7999
+ capturedCount64: "0",
8000
+ deliveredCount64: "0",
8001
+ droppedBufferCount64: "0",
8002
+ droppedSinkCount64: "0"
8003
+ });
8004
+ }
8005
+ function aggregateObservationDeliveryStats(nodes) {
8006
+ let lastBatchSequence = -1n;
8007
+ let captured = 0n;
8008
+ let delivered = 0n;
8009
+ let droppedBuffer = 0n;
8010
+ let droppedSink = 0n;
8011
+ for (const node of nodes) {
8012
+ const stats = node.observationDeliveryStats ?? emptyObservationDeliveryStats();
8013
+ lastBatchSequence = maxBigInt(lastBatchSequence, parseObservationDecimal(stats.lastBatchSequence64, -1n));
8014
+ captured += parseObservationDecimal(stats.capturedCount64);
8015
+ delivered += parseObservationDecimal(stats.deliveredCount64);
8016
+ droppedBuffer += parseObservationDecimal(stats.droppedBufferCount64);
8017
+ droppedSink += parseObservationDecimal(stats.droppedSinkCount64);
8018
+ }
8019
+ return normalizeObservationDeliveryStats({
8020
+ lastBatchSequence64: lastBatchSequence.toString(),
8021
+ capturedCount64: captured.toString(),
8022
+ deliveredCount64: delivered.toString(),
8023
+ droppedBufferCount64: droppedBuffer.toString(),
8024
+ droppedSinkCount64: droppedSink.toString()
8025
+ });
8026
+ }
8027
+ function parseObservationDecimal(value, fallback = 0n) {
8028
+ try {
8029
+ return BigInt(value);
8030
+ }
8031
+ catch {
8032
+ return fallback;
8033
+ }
8034
+ }
8035
+ function aggregateNodeStats(testInfo, coordinatorNodeInfo, nodes, requireHistograms = false) {
6096
8036
  if (!nodes.length) {
6097
8037
  return buildEmptyNodeStats({
6098
8038
  startedUtc: testInfo.createdUtc,
@@ -6101,12 +8041,22 @@ function aggregateNodeStats(testInfo, coordinatorNodeInfo, nodes) {
6101
8041
  testInfo
6102
8042
  });
6103
8043
  }
6104
- const scenarioStats = aggregateScenarioStats(nodes);
8044
+ const scenarioStats = aggregateScenarioStats(nodes, requireHistograms);
6105
8045
  const stepStats = scenarioStats.flatMap((value) => value.stepStats);
6106
8046
  const metrics = aggregateMetricStats(nodes);
6107
8047
  const thresholds = aggregateThresholds(nodes);
6108
8048
  const pluginsData = aggregatePluginsData(nodes);
6109
8049
  const completedUtc = new Date().toISOString();
8050
+ const schedulerSegments = nodes.flatMap((value) => value.schedulerSegments ?? []);
8051
+ const schedulerStatsRows = nodes.flatMap((value) => value.schedulerStats ? [value.schedulerStats] : []);
8052
+ const schedulerStats = schedulerSegments.length || schedulerStatsRows.length
8053
+ ? {
8054
+ configuredMaxInFlight: schedulerStatsRows.reduce((maximum, value) => Math.max(maximum, value.configuredMaxInFlight), 0),
8055
+ maxInFlightObserved: schedulerStatsRows.reduce((maximum, value) => Math.max(maximum, value.maxInFlightObserved), 0),
8056
+ currentInFlight: schedulerStatsRows.reduce((sum, value) => sum + value.currentInFlight, 0),
8057
+ segments: schedulerSegments
8058
+ }
8059
+ : undefined;
6110
8060
  return attachNodeStatsAliases({
6111
8061
  startedUtc: testInfo.createdUtc,
6112
8062
  completedUtc,
@@ -6129,6 +8079,12 @@ function aggregateNodeStats(testInfo, coordinatorNodeInfo, nodes) {
6129
8079
  sinkErrors: [],
6130
8080
  reportFiles: [],
6131
8081
  logFiles: mergeStringArrays(...nodes.map((value) => value.logFiles ?? [])),
8082
+ generatorWarnings: nodes.flatMap((value) => value.generatorWarnings ?? []),
8083
+ ...(schedulerSegments.length ? { schedulerSegments } : {}),
8084
+ ...(schedulerStats ? { schedulerStats } : {}),
8085
+ [LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS]: mergeLoadEngineV2SchedulerDistributions(nodes.flatMap((value) => value[LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS] ?? [])),
8086
+ observationDeliveryStats: aggregateObservationDeliveryStats(nodes),
8087
+ reportingComplete: nodes.every((value) => value.reportingComplete ?? false),
6132
8088
  findScenarioStats: (scenarioName) => scenarioStats.find((value) => value.scenarioName === scenarioName),
6133
8089
  getScenarioStats: (scenarioName) => {
6134
8090
  const value = scenarioStats.find((scenario) => scenario.scenarioName === scenarioName);
@@ -6139,7 +8095,7 @@ function aggregateNodeStats(testInfo, coordinatorNodeInfo, nodes) {
6139
8095
  }
6140
8096
  });
6141
8097
  }
6142
- function aggregateScenarioStats(nodes) {
8098
+ function aggregateScenarioStats(nodes, requireHistograms = false) {
6143
8099
  const grouped = new Map();
6144
8100
  for (const node of nodes) {
6145
8101
  for (const scenario of node.scenarioStats) {
@@ -6153,9 +8109,12 @@ function aggregateScenarioStats(nodes) {
6153
8109
  .map((items) => {
6154
8110
  const allRequestCount = items.reduce((sum, value) => sum + value.allRequestCount, 0);
6155
8111
  const durationMs = items.reduce((max, value) => Math.max(max, value.durationMs), 0);
6156
- const ok = aggregateMeasurementStats(items.map((value) => value.ok), allRequestCount, durationMs);
6157
- const fail = aggregateMeasurementStats(items.map((value) => value.fail), allRequestCount, durationMs);
6158
- const stepStats = aggregateStepStats(items);
8112
+ const ok = aggregateMeasurementStats(items.map((value) => value.ok), allRequestCount, durationMs, requireHistograms);
8113
+ const fail = aggregateMeasurementStats(items.map((value) => value.fail), allRequestCount, durationMs, requireHistograms);
8114
+ const allMeasurement = requireHistograms
8115
+ ? aggregateMeasurementStats([ok, fail], allRequestCount, durationMs, true)
8116
+ : undefined;
8117
+ const stepStats = aggregateStepStats(items, requireHistograms);
6159
8118
  const scenarioName = items[0]?.scenarioName ?? "";
6160
8119
  const currentOperation = selectScenarioOperation(items.map((value) => value.currentOperation));
6161
8120
  const loadSimulationStats = items.find((value) => value.loadSimulationStats.simulationName)?.loadSimulationStats ?? {
@@ -6180,6 +8139,7 @@ function aggregateScenarioStats(nodes) {
6180
8139
  durationMs,
6181
8140
  ok,
6182
8141
  fail,
8142
+ ...(allMeasurement ? { allMeasurement } : {}),
6183
8143
  loadSimulationStats,
6184
8144
  sortIndex: Math.min(...items.map((value) => value.sortIndex)),
6185
8145
  stepStats,
@@ -6195,7 +8155,7 @@ function aggregateScenarioStats(nodes) {
6195
8155
  return attachScenarioStatsAliases(scenario);
6196
8156
  });
6197
8157
  }
6198
- function aggregateStepStats(scenarios) {
8158
+ function aggregateStepStats(scenarios, requireHistograms = false) {
6199
8159
  const grouped = new Map();
6200
8160
  for (const scenario of scenarios) {
6201
8161
  for (const step of scenario.stepStats) {
@@ -6209,8 +8169,11 @@ function aggregateStepStats(scenarios) {
6209
8169
  return Array.from(grouped.values())
6210
8170
  .sort((left, right) => Math.min(...left.map((value) => value.sortIndex)) - Math.min(...right.map((value) => value.sortIndex)))
6211
8171
  .map((items) => {
6212
- const ok = aggregateMeasurementStats(items.map((value) => value.ok), allScenarioRequests, scenarioDurationMs);
6213
- const fail = aggregateMeasurementStats(items.map((value) => value.fail), allScenarioRequests, scenarioDurationMs);
8172
+ const ok = aggregateMeasurementStats(items.map((value) => value.ok), allScenarioRequests, scenarioDurationMs, requireHistograms);
8173
+ const fail = aggregateMeasurementStats(items.map((value) => value.fail), allScenarioRequests, scenarioDurationMs, requireHistograms);
8174
+ const allMeasurement = requireHistograms
8175
+ ? aggregateMeasurementStats([ok, fail], allScenarioRequests, scenarioDurationMs, true)
8176
+ : undefined;
6214
8177
  const requestCount = ok.request.count + fail.request.count;
6215
8178
  return {
6216
8179
  scenarioName: items[0]?.scenarioName ?? "",
@@ -6226,14 +8189,53 @@ function aggregateStepStats(scenarios) {
6226
8189
  statusCodes: aggregateStatusCodeCounts(ok.statusCodes, fail.statusCodes),
6227
8190
  ok,
6228
8191
  fail,
8192
+ ...(allMeasurement ? { allMeasurement } : {}),
6229
8193
  sortIndex: Math.min(...items.map((value) => value.sortIndex))
6230
8194
  };
6231
8195
  });
6232
8196
  }
6233
- function aggregateMeasurementStats(measurements, allRequestCount, durationMs) {
8197
+ function aggregateMeasurementStats(measurements, allRequestCount, durationMs, requireHistograms = false) {
6234
8198
  if (!measurements.length) {
6235
8199
  return buildMeasurementPlaceholder(0, allRequestCount, durationMs);
6236
8200
  }
8201
+ if (measurements.every((measurement) => measurement.histogramSidecar)) {
8202
+ const latency = load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(measurements[0].histogramSidecar.latency);
8203
+ const size = load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(measurements[0].histogramSidecar.size);
8204
+ for (const measurement of measurements.slice(1)) {
8205
+ latency.merge(load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(measurement.histogramSidecar.latency));
8206
+ size.merge(load_engine_v2_js_1.LoadStrikeHistogramV1.fromSidecar(measurement.histogramSidecar.size));
8207
+ }
8208
+ const statusCodes = new Map();
8209
+ for (const measurement of measurements) {
8210
+ for (const status of measurement.statusCodes) {
8211
+ const key = `${status.statusCode}|${status.message}|${status.isError ? "1" : "0"}`;
8212
+ const current = statusCodes.get(key);
8213
+ if (current)
8214
+ current.count += status.count;
8215
+ else
8216
+ statusCodes.set(key, {
8217
+ statusCode: status.statusCode,
8218
+ message: status.message,
8219
+ isError: status.isError,
8220
+ count: status.count
8221
+ });
8222
+ }
8223
+ }
8224
+ const sumSidecar = (key) => Number(measurements.reduce((sum, measurement) => sum + BigInt(measurement.histogramSidecar[key]), 0n));
8225
+ return buildHistogramMeasurement({
8226
+ count: Number(latency.count),
8227
+ allBytes: measurements.reduce((sum, measurement) => sum + measurement.dataTransfer.allBytes, 0),
8228
+ latency,
8229
+ size,
8230
+ statusCodes,
8231
+ lessOrEq800: sumSidecar("lessOrEq80064"),
8232
+ more800Less1200: sumSidecar("more800Less120064"),
8233
+ moreOrEq1200: sumSidecar("moreOrEq120064")
8234
+ }, allRequestCount, durationMs);
8235
+ }
8236
+ if (requireHistograms) {
8237
+ throw new Error("Load Engine V2 aggregation requires canonical histogram state for every node measurement.");
8238
+ }
6237
8239
  const weights = measurements.map((value) => value.request.count);
6238
8240
  const totalCount = measurements.reduce((sum, value) => sum + value.request.count, 0);
6239
8241
  return {
@@ -8055,6 +10057,68 @@ function normalizeOptionalReportFormats(value) {
8055
10057
  const normalized = normalizeReportFormats(value);
8056
10058
  return normalized.length ? normalized : undefined;
8057
10059
  }
10060
+ function normalizeDeclaredStepNames(value) {
10061
+ if (!Array.isArray(value)) {
10062
+ throw new TypeError("Declared step names must be provided as text values.");
10063
+ }
10064
+ const seen = new Set();
10065
+ const normalized = [];
10066
+ for (const entry of value) {
10067
+ if (typeof entry !== "string" || !entry.trim()) {
10068
+ throw new Error("Declared step name must be non-empty text.");
10069
+ }
10070
+ const stepName = entry.trim();
10071
+ for (let index = 0; index < stepName.length; index += 1) {
10072
+ const code = stepName.charCodeAt(index);
10073
+ if (code >= 0xd800 && code <= 0xdbff) {
10074
+ const next = stepName.charCodeAt(index + 1);
10075
+ if (!(next >= 0xdc00 && next <= 0xdfff)) {
10076
+ throw new Error("Declared step name contains an invalid Unicode scalar.");
10077
+ }
10078
+ index += 1;
10079
+ }
10080
+ else if (code >= 0xdc00 && code <= 0xdfff) {
10081
+ throw new Error("Declared step name contains an invalid Unicode scalar.");
10082
+ }
10083
+ }
10084
+ if (!seen.has(stepName)) {
10085
+ seen.add(stepName);
10086
+ normalized.push(stepName);
10087
+ }
10088
+ }
10089
+ return normalized;
10090
+ }
10091
+ function resolveIterationObservationSettings(options) {
10092
+ return {
10093
+ flushIntervalMs: options.iterationObservationFlushIntervalSeconds === undefined
10094
+ ? iteration_observations_js_1.DEFAULT_ITERATION_OBSERVATION_SETTINGS.flushIntervalMs
10095
+ : options.iterationObservationFlushIntervalSeconds * 1000,
10096
+ maxBufferBytes: options.maxIterationObservationBufferBytes
10097
+ ?? iteration_observations_js_1.DEFAULT_ITERATION_OBSERVATION_SETTINGS.maxBufferBytes,
10098
+ maxObservationsPerBatch: options.maxIterationObservationsPerBatch
10099
+ ?? iteration_observations_js_1.DEFAULT_ITERATION_OBSERVATION_SETTINGS.maxObservationsPerBatch,
10100
+ maxBatchBytes: options.maxIterationObservationBatchBytes
10101
+ ?? iteration_observations_js_1.DEFAULT_ITERATION_OBSERVATION_SETTINGS.maxBatchBytes,
10102
+ sinkQueueDepth: options.iterationObservationSinkQueueDepth
10103
+ ?? iteration_observations_js_1.DEFAULT_ITERATION_OBSERVATION_SETTINGS.sinkQueueDepth,
10104
+ sinkParallelism: options.iterationObservationSinkParallelism
10105
+ ?? iteration_observations_js_1.DEFAULT_ITERATION_OBSERVATION_SETTINGS.sinkParallelism,
10106
+ drainTimeoutMs: options.iterationObservationDrainTimeoutSeconds === undefined
10107
+ ? iteration_observations_js_1.DEFAULT_ITERATION_OBSERVATION_SETTINGS.drainTimeoutMs
10108
+ : options.iterationObservationDrainTimeoutSeconds * 1000
10109
+ };
10110
+ }
10111
+ function validateRunContextIterationObservationSettings(values) {
10112
+ (0, iteration_observations_js_1.validateIterationObservationSettings)(resolveIterationObservationSettings({
10113
+ iterationObservationFlushIntervalSeconds: values.IterationObservationFlushIntervalSeconds,
10114
+ maxIterationObservationBufferBytes: values.MaxIterationObservationBufferBytes,
10115
+ maxIterationObservationsPerBatch: values.MaxIterationObservationsPerBatch,
10116
+ maxIterationObservationBatchBytes: values.MaxIterationObservationBatchBytes,
10117
+ iterationObservationSinkQueueDepth: values.IterationObservationSinkQueueDepth,
10118
+ iterationObservationSinkParallelism: values.IterationObservationSinkParallelism,
10119
+ iterationObservationDrainTimeoutSeconds: values.IterationObservationDrainTimeoutSeconds
10120
+ }));
10121
+ }
8058
10122
  function assertNoDisableLicenseEnforcementOption(value, source) {
8059
10123
  if (value == null || typeof value !== "object" || Array.isArray(value)) {
8060
10124
  return;
@@ -8073,12 +10137,121 @@ function normalizeRunContextCollectionShapes(values) {
8073
10137
  TargetScenarios: normalizeOptionalStringArray(values.TargetScenarios),
8074
10138
  AgentTargetScenarios: normalizeOptionalStringArray(values.AgentTargetScenarios),
8075
10139
  CoordinatorTargetScenarios: normalizeOptionalStringArray(values.CoordinatorTargetScenarios),
10140
+ ExpectedAgentIds: normalizeOptionalStringArray(values.ExpectedAgentIds),
8076
10141
  ReportFormats: normalizeOptionalReportFormats(values.ReportFormats)
8077
10142
  };
8078
10143
  validateNamedReportingSinks(normalized.ReportingSinks ?? []);
8079
10144
  validateNamedWorkerPlugins(normalized.WorkerPlugins ?? []);
10145
+ validateLoadEngineV2Options(normalized.LoadEngineContractVersion, normalized.MaxInFlight);
10146
+ validateRunContextIterationObservationSettings(normalized);
8080
10147
  return normalized;
8081
10148
  }
10149
+ function normalizeAliasStringRecord(value) {
10150
+ const source = asAliasRecord(value);
10151
+ const output = {};
10152
+ for (const [key, entry] of Object.entries(source)) {
10153
+ output[key] = String(entry);
10154
+ }
10155
+ return output;
10156
+ }
10157
+ function attachGeneratorWarningAliases(value) {
10158
+ const source = asAliasRecord(value);
10159
+ const projected = {
10160
+ code: pickAliasString(source, "code", "Code"),
10161
+ ...(hasAliasValue(source, "sinkName", "SinkName")
10162
+ ? { sinkName: pickAliasString(source, "sinkName", "SinkName") }
10163
+ : {}),
10164
+ scenarioName: pickAliasString(source, "scenarioName", "ScenarioName"),
10165
+ ...(hasAliasValue(source, "scenarioIndex", "ScenarioIndex")
10166
+ ? { scenarioIndex: pickAliasNumber(source, "scenarioIndex", "ScenarioIndex") }
10167
+ : {}),
10168
+ simulationIndex: pickAliasNumber(source, "simulationIndex", "SimulationIndex"),
10169
+ ...(hasAliasValue(source, "simulationKind", "SimulationKind")
10170
+ ? { simulationKind: pickAliasString(source, "simulationKind", "SimulationKind") }
10171
+ : {}),
10172
+ count64: pickAliasString(source, "count64", "Count64"),
10173
+ message: pickAliasString(source, "message", "Message"),
10174
+ firstObservedUtcNs: pickAliasString(source, "firstObservedUtcNs", "FirstObservedUtcNs"),
10175
+ lastObservedUtcNs: pickAliasString(source, "lastObservedUtcNs", "LastObservedUtcNs")
10176
+ };
10177
+ return attachAliasMap(projected, {
10178
+ Code: "code",
10179
+ SinkName: "sinkName",
10180
+ ScenarioName: "scenarioName",
10181
+ ScenarioIndex: "scenarioIndex",
10182
+ SimulationIndex: "simulationIndex",
10183
+ SimulationKind: "simulationKind",
10184
+ Count64: "count64",
10185
+ Message: "message",
10186
+ FirstObservedUtcNs: "firstObservedUtcNs",
10187
+ LastObservedUtcNs: "lastObservedUtcNs"
10188
+ });
10189
+ }
10190
+ function normalizeSchedulerSegment(value) {
10191
+ const source = asAliasRecord(value);
10192
+ return {
10193
+ scenarioName: pickAliasString(source, "scenarioName", "ScenarioName"),
10194
+ scenarioIndex: pickAliasNumber(source, "scenarioIndex", "ScenarioIndex"),
10195
+ simulationIndex: pickAliasNumber(source, "simulationIndex", "SimulationIndex"),
10196
+ kind: pickAliasString(source, "kind", "Kind"),
10197
+ shardIndex: pickAliasNumber(source, "shardIndex", "ShardIndex"),
10198
+ shardCount: Math.max(pickAliasNumber(source, "shardCount", "ShardCount"), 1),
10199
+ plannedIterations64: pickAliasString(source, "plannedIterations64", "PlannedIterations64"),
10200
+ dueIterations64: pickAliasString(source, "dueIterations64", "DueIterations64"),
10201
+ startedIterations64: pickAliasString(source, "startedIterations64", "StartedIterations64"),
10202
+ completedIterations64: pickAliasString(source, "completedIterations64", "CompletedIterations64"),
10203
+ droppedIterations64: pickAliasString(source, "droppedIterations64", "DroppedIterations64"),
10204
+ unreachedIterations64: pickAliasString(source, "unreachedIterations64", "UnreachedIterations64"),
10205
+ requestedWorkerSlots64: pickAliasString(source, "requestedWorkerSlots64", "RequestedWorkerSlots64"),
10206
+ startedWorkerSlots64: pickAliasString(source, "startedWorkerSlots64", "StartedWorkerSlots64"),
10207
+ unavailableWorkerSlots64: pickAliasString(source, "unavailableWorkerSlots64", "UnavailableWorkerSlots64"),
10208
+ dropReasons: normalizeAliasStringRecord(pickAliasValue(source, "dropReasons", "DropReasons")),
10209
+ unavailableWorkerReasons: normalizeAliasStringRecord(pickAliasValue(source, "unavailableWorkerReasons", "UnavailableWorkerReasons")),
10210
+ deliveryPercent: pickAliasNumber(source, "deliveryPercent", "DeliveryPercent"),
10211
+ accountingComplete: pickAliasBoolean(source, "accountingComplete", "AccountingComplete")
10212
+ };
10213
+ }
10214
+ function normalizeSchedulerStats(value) {
10215
+ const source = asAliasRecord(value);
10216
+ return {
10217
+ configuredMaxInFlight: pickAliasNumber(source, "configuredMaxInFlight", "ConfiguredMaxInFlight"),
10218
+ maxInFlightObserved: pickAliasNumber(source, "maxInFlightObserved", "MaxInFlightObserved"),
10219
+ currentInFlight: pickAliasNumber(source, "currentInFlight", "CurrentInFlight"),
10220
+ segments: pickAliasArray(source, "segments", "Segments").map(normalizeSchedulerSegment)
10221
+ };
10222
+ }
10223
+ function normalizeObservationDeliveryStats(value) {
10224
+ const source = asAliasRecord(value);
10225
+ return attachAliasMap({
10226
+ lastBatchSequence64: pickAliasString(source, "lastBatchSequence64", "LastBatchSequence64"),
10227
+ capturedCount64: pickAliasString(source, "capturedCount64", "CapturedCount64"),
10228
+ deliveredCount64: pickAliasString(source, "deliveredCount64", "DeliveredCount64"),
10229
+ droppedBufferCount64: pickAliasString(source, "droppedBufferCount64", "DroppedBufferCount64"),
10230
+ droppedSinkCount64: pickAliasString(source, "droppedSinkCount64", "DroppedSinkCount64")
10231
+ }, {
10232
+ LastBatchSequence64: "lastBatchSequence64",
10233
+ CapturedCount64: "capturedCount64",
10234
+ DeliveredCount64: "deliveredCount64",
10235
+ DroppedBufferCount64: "droppedBufferCount64",
10236
+ DroppedSinkCount64: "droppedSinkCount64"
10237
+ });
10238
+ }
10239
+ function validateLoadEngineV2Options(contractVersion, maxInFlight) {
10240
+ if (contractVersion !== undefined && contractVersion !== 1 && contractVersion !== 2) {
10241
+ throw new RangeError("Load engine contract version must be either 1 or 2.");
10242
+ }
10243
+ if (maxInFlight !== undefined) {
10244
+ validateV2MaxInFlight(contractVersion, maxInFlight);
10245
+ }
10246
+ }
10247
+ function validateV2MaxInFlight(contractVersion, maxInFlight) {
10248
+ if (contractVersion !== 2) {
10249
+ throw new Error("MaxInFlight is available only when Load Engine V2 is selected.");
10250
+ }
10251
+ if (!Number.isSafeInteger(maxInFlight) || maxInFlight < 1 || maxInFlight > 1000000) {
10252
+ throw new RangeError("MaxInFlight must be an integer from 1 through 1000000.");
10253
+ }
10254
+ }
8082
10255
  function normalizeRunnerOptionCollectionShapes(options) {
8083
10256
  assertNoDisableLicenseEnforcementOption(options, "LoadStrikeRunner");
8084
10257
  const normalized = {
@@ -8086,10 +10259,13 @@ function normalizeRunnerOptionCollectionShapes(options) {
8086
10259
  targetScenarios: normalizeOptionalStringArray(options.targetScenarios),
8087
10260
  agentTargetScenarios: normalizeOptionalStringArray(options.agentTargetScenarios),
8088
10261
  coordinatorTargetScenarios: normalizeOptionalStringArray(options.coordinatorTargetScenarios),
10262
+ expectedAgentIds: normalizeOptionalStringArray(options.expectedAgentIds),
8089
10263
  reportFormats: normalizeOptionalReportFormats(options.reportFormats)
8090
10264
  };
8091
10265
  validateNamedReportingSinks(normalized.reportingSinks ?? []);
8092
10266
  validateNamedWorkerPlugins(normalized.workerPlugins ?? []);
10267
+ validateLoadEngineV2Options(normalized.loadEngineContractVersion, normalized.maxInFlight);
10268
+ (0, iteration_observations_js_1.validateIterationObservationSettings)(resolveIterationObservationSettings(normalized));
8093
10269
  return normalized;
8094
10270
  }
8095
10271
  function normalizedRuntimePolicyErrorMode(value) {
@@ -8153,6 +10329,7 @@ function extractContextOverridesFromConfig(config) {
8153
10329
  setString("ReportFileName", "ReportFileName", "LoadStrike:ReportFileName");
8154
10330
  setString("ClusterId", "ClusterId", "LoadStrike:ClusterId");
8155
10331
  setString("AgentGroup", "AgentGroup", "LoadStrike:AgentGroup");
10332
+ setString("AgentId", "AgentId", "LoadStrike:AgentId");
8156
10333
  setString("NatsServerUrl", "NatsServerUrl", "LoadStrike:NatsServerUrl");
8157
10334
  setString("RunnerKey", "RunnerKey", "LoadStrike:RunnerKey");
8158
10335
  setString("RuntimePolicyErrorMode", "RuntimePolicyErrorMode", "LoadStrike:RuntimePolicyErrorMode");
@@ -8182,6 +10359,13 @@ function extractContextOverridesFromConfig(config) {
8182
10359
  }
8183
10360
  }
8184
10361
  setPositiveNumber("ReportingIntervalSeconds", "ReportingIntervalSeconds", "LoadStrike:ReportingIntervalSeconds");
10362
+ setPositiveNumber("IterationObservationFlushIntervalSeconds", "IterationObservationFlushInterval", "IterationObservationFlushIntervalSeconds", "LoadStrike:IterationObservationFlushInterval");
10363
+ setPositiveNumber("MaxIterationObservationBufferBytes", "MaxIterationObservationBufferBytes", "LoadStrike:MaxIterationObservationBufferBytes");
10364
+ setPositiveNumber("MaxIterationObservationsPerBatch", "MaxIterationObservationsPerBatch", "LoadStrike:MaxIterationObservationsPerBatch");
10365
+ setPositiveNumber("MaxIterationObservationBatchBytes", "MaxIterationObservationBatchBytes", "LoadStrike:MaxIterationObservationBatchBytes");
10366
+ setPositiveNumber("IterationObservationSinkQueueDepth", "IterationObservationSinkQueueDepth", "LoadStrike:IterationObservationSinkQueueDepth");
10367
+ setPositiveNumber("IterationObservationSinkParallelism", "IterationObservationSinkParallelism", "LoadStrike:IterationObservationSinkParallelism");
10368
+ setPositiveNumber("IterationObservationDrainTimeoutSeconds", "IterationObservationDrainTimeout", "IterationObservationDrainTimeoutSeconds", "LoadStrike:IterationObservationDrainTimeout");
8185
10369
  setPositiveNumber("ScenarioCompletionTimeoutSeconds", "ScenarioCompletionTimeoutSeconds", "LoadStrike:ScenarioCompletionTimeoutSeconds");
8186
10370
  setPositiveNumber("ClusterCommandTimeoutSeconds", "ClusterCommandTimeoutSeconds", "LoadStrike:ClusterCommandTimeoutSeconds");
8187
10371
  setPositiveNumber("LicenseValidationTimeoutSeconds", "LicenseValidationTimeoutSeconds", "LoadStrike:LicenseValidation:TimeoutSeconds");
@@ -8189,6 +10373,14 @@ function extractContextOverridesFromConfig(config) {
8189
10373
  if (reportingIntervalMs > 0) {
8190
10374
  patch.ReportingIntervalSeconds = reportingIntervalMs / 1000;
8191
10375
  }
10376
+ const observationFlushIntervalMs = toNumber(pick("IterationObservationFlushIntervalMs", "LoadStrike:IterationObservationFlushIntervalMs"));
10377
+ if (observationFlushIntervalMs > 0) {
10378
+ patch.IterationObservationFlushIntervalSeconds = observationFlushIntervalMs / 1000;
10379
+ }
10380
+ const observationDrainTimeoutMs = toNumber(pick("IterationObservationDrainTimeoutMs", "LoadStrike:IterationObservationDrainTimeoutMs"));
10381
+ if (observationDrainTimeoutMs > 0) {
10382
+ patch.IterationObservationDrainTimeoutSeconds = observationDrainTimeoutMs / 1000;
10383
+ }
8192
10384
  const scenarioCompletionTimeoutMs = toNumber(pick("ScenarioCompletionTimeoutMs", "LoadStrike:ScenarioCompletionTimeoutMs"));
8193
10385
  if (scenarioCompletionTimeoutMs > 0) {
8194
10386
  patch.ScenarioCompletionTimeoutSeconds = scenarioCompletionTimeoutMs / 1000;
@@ -8233,6 +10425,10 @@ function extractContextOverridesFromConfig(config) {
8233
10425
  if (agentTargetScenarios.length) {
8234
10426
  patch.AgentTargetScenarios = agentTargetScenarios;
8235
10427
  }
10428
+ const expectedAgentIds = normalizeStringArray(pick("ExpectedAgentIds", "LoadStrike:ExpectedAgentIds"));
10429
+ if (expectedAgentIds.length) {
10430
+ patch.ExpectedAgentIds = expectedAgentIds;
10431
+ }
8236
10432
  const coordinatorTargetScenarios = normalizeStringArray(pick("CoordinatorTargetScenarios", "LoadStrike:CoordinatorTargetScenarios"));
8237
10433
  if (coordinatorTargetScenarios.length) {
8238
10434
  patch.CoordinatorTargetScenarios = coordinatorTargetScenarios;
@@ -8295,10 +10491,14 @@ function toRunContext(options) {
8295
10491
  const normalized = normalizeRunnerOptionCollectionShapes(options);
8296
10492
  return {
8297
10493
  ConsoleMetricsEnabled: normalized.displayConsoleMetrics,
10494
+ LoadEngineContractVersion: normalized.loadEngineContractVersion,
10495
+ MaxInFlight: normalized.maxInFlight,
8298
10496
  NodeType: normalized.nodeType,
8299
10497
  LocalDevClusterEnabled: normalized.localDevClusterEnabled,
8300
10498
  AgentGroup: normalized.agentGroup,
8301
10499
  AgentsCount: normalized.agentsCount,
10500
+ AgentId: normalized.agentId,
10501
+ ExpectedAgentIds: normalized.expectedAgentIds,
8302
10502
  TargetScenarios: normalized.targetScenarios,
8303
10503
  AgentTargetScenarios: normalized.agentTargetScenarios,
8304
10504
  CoordinatorTargetScenarios: normalized.coordinatorTargetScenarios,
@@ -8317,6 +10517,13 @@ function toRunContext(options) {
8317
10517
  ReportFolderPath: normalized.reportFolderPath,
8318
10518
  ReportFormats: normalized.reportFormats,
8319
10519
  ReportingIntervalSeconds: normalized.reportingIntervalSeconds,
10520
+ IterationObservationFlushIntervalSeconds: normalized.iterationObservationFlushIntervalSeconds,
10521
+ MaxIterationObservationBufferBytes: normalized.maxIterationObservationBufferBytes,
10522
+ MaxIterationObservationsPerBatch: normalized.maxIterationObservationsPerBatch,
10523
+ MaxIterationObservationBatchBytes: normalized.maxIterationObservationBatchBytes,
10524
+ IterationObservationSinkQueueDepth: normalized.iterationObservationSinkQueueDepth,
10525
+ IterationObservationSinkParallelism: normalized.iterationObservationSinkParallelism,
10526
+ IterationObservationDrainTimeoutSeconds: normalized.iterationObservationDrainTimeoutSeconds,
8320
10527
  MinimumLogLevel: normalized.minimumLogLevel,
8321
10528
  LoggerConfig: normalized.loggerConfig,
8322
10529
  ReportingSinks: normalized.reportingSinks,
@@ -8331,7 +10538,9 @@ function toRunContext(options) {
8331
10538
  CustomSettings: normalized.customSettings,
8332
10539
  GlobalCustomSettings: normalized.globalCustomSettings,
8333
10540
  AgentExecutionToken: normalized.agentExecutionToken,
8334
- AgentCommandId: normalized.agentCommandId
10541
+ AgentCommandId: normalized.agentCommandId,
10542
+ ClusterShardIndex: normalized.clusterShardIndex,
10543
+ ClusterShardCount: normalized.clusterShardCount
8335
10544
  };
8336
10545
  }
8337
10546
  class RuntimePolicyCallbackError extends Error {
@@ -8419,11 +10628,15 @@ function looksLikeRunContext(value) {
8419
10628
  const keys = new Set(Object.keys(value));
8420
10629
  return [
8421
10630
  "ConsoleMetricsEnabled",
10631
+ "LoadEngineContractVersion",
10632
+ "MaxInFlight",
8422
10633
  "LocalDevClusterEnabled",
8423
10634
  "ConfigPath",
8424
10635
  "InfraConfigPath",
8425
10636
  "AgentGroup",
8426
10637
  "AgentsCount",
10638
+ "AgentId",
10639
+ "ExpectedAgentIds",
8427
10640
  "AgentTargetScenarios",
8428
10641
  "ClusterId",
8429
10642
  "CoordinatorTargetScenarios",
@@ -8438,6 +10651,13 @@ function looksLikeRunContext(value) {
8438
10651
  "ReportFolderPath",
8439
10652
  "ReportFormats",
8440
10653
  "ReportingIntervalSeconds",
10654
+ "IterationObservationFlushIntervalSeconds",
10655
+ "MaxIterationObservationBufferBytes",
10656
+ "MaxIterationObservationsPerBatch",
10657
+ "MaxIterationObservationBatchBytes",
10658
+ "IterationObservationSinkQueueDepth",
10659
+ "IterationObservationSinkParallelism",
10660
+ "IterationObservationDrainTimeoutSeconds",
8441
10661
  "ReportingSinks",
8442
10662
  "SinkRetryCount",
8443
10663
  "SinkRetryBackoffMs",
@@ -8579,6 +10799,18 @@ function resolveSinkSaveRunResult(sink) {
8579
10799
  ? method.bind(sink)
8580
10800
  : undefined;
8581
10801
  }
10802
+ function resolveSinkSaveIterationBatch(sink) {
10803
+ const method = sink.saveIterationBatch ?? sink.SaveIterationBatch;
10804
+ return typeof method === "function"
10805
+ ? method.bind(sink)
10806
+ : undefined;
10807
+ }
10808
+ function resolveSinkCompleteIterationObservationStream(sink) {
10809
+ const method = sink.completeIterationObservationStream ?? sink.CompleteIterationObservationStream;
10810
+ return typeof method === "function"
10811
+ ? method.bind(sink)
10812
+ : undefined;
10813
+ }
8582
10814
  function resolveSinkStop(sink) {
8583
10815
  const method = sink.stop ?? sink.Stop;
8584
10816
  return typeof method === "function"
@@ -8996,13 +11228,17 @@ exports.__loadstrikeTestExports = {
8996
11228
  ManagedScenarioTrackingRuntime,
8997
11229
  ScenarioStatsAccumulator,
8998
11230
  StepStatsAccumulator,
11231
+ planRuntimeClusterAssignments,
8999
11232
  TrackingFieldSelector: correlation_js_1.TrackingFieldSelector,
9000
11233
  addCorrelationRow,
9001
11234
  addFailedResponseRow,
9002
11235
  aggregateNodeStats,
11236
+ aggregateMeasurementStats,
9003
11237
  asRecord,
9004
11238
  assertNoDisableLicenseEnforcementOption,
9005
11239
  buildEmptyNodeStats,
11240
+ buildRuntimeLoadEngineV2HistogramArtifact,
11241
+ buildRuntimeLoadEngineV2Plan,
9006
11242
  buildGroupedCorrelationRows,
9007
11243
  buildMeasurementPlaceholder,
9008
11244
  buildRichHtmlReport,
@@ -9075,7 +11311,9 @@ exports.__loadstrikeTestExports = {
9075
11311
  resolveSinkName,
9076
11312
  resolveSinkSaveRealtimeMetrics,
9077
11313
  resolveSinkSaveRealtimeStats,
11314
+ resolveSinkSaveIterationBatch,
9078
11315
  resolveSinkSaveRunResult,
11316
+ resolveSinkCompleteIterationObservationStream,
9079
11317
  resolveSinkStart,
9080
11318
  resolveSinkStop,
9081
11319
  resolveWorkerPlugins,
@@ -9096,6 +11334,7 @@ exports.__loadstrikeTestExports = {
9096
11334
  tryParseNodeTypeToken,
9097
11335
  tryReadConfigValue,
9098
11336
  validateNamedReportingSinks,
11337
+ validateLoadEngineV2ScenarioFeatures,
9099
11338
  validateRegisteredScenarios,
9100
11339
  validateRuntimeRedisCorrelationStoreConfiguration,
9101
11340
  validateRuntimeTrackingConfiguration,