@loadstrike/loadstrike-sdk 1.0.31001 → 1.0.31601

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.
@@ -13,6 +13,8 @@ const cluster_js_1 = require("./cluster.js");
13
13
  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
+ const report_history_js_1 = require("./report-history.js");
17
+ const local_report_input_js_1 = require("./local-report-input.js");
16
18
  const sinks_js_1 = require("./sinks.js");
17
19
  const load_engine_v2_js_1 = require("./load-engine-v2.js");
18
20
  const iteration_observations_js_1 = require("./iteration-observations.js");
@@ -291,6 +293,28 @@ class MeasurementAccumulator {
291
293
  moreOrEq1200: left.moreOrEq1200 + right.moreOrEq1200
292
294
  }, allRequestCount, durationMs);
293
295
  }
296
+ /**
297
+ * Projects only the cumulative fields used by the private HTML-report
298
+ * history. Legacy distributions deliberately omit latency so this cadence
299
+ * path never scans their retained per-request arrays.
300
+ */
301
+ buildReportHistoryMeasurement() {
302
+ return projectNativeReportHistoryMeasurement(this.count, this.allBytes, this.useHistogram ? this.latencyHistogram : undefined);
303
+ }
304
+ /**
305
+ * Combines only bounded native latency state plus exact counters. It does
306
+ * not materialize status rows or any public measurement DTO.
307
+ */
308
+ buildCombinedReportHistoryMeasurement(other) {
309
+ const count = this.count + other.count;
310
+ const bytes = this.allBytes + other.allBytes;
311
+ if (!this.useHistogram || !other.useHistogram) {
312
+ return projectNativeReportHistoryMeasurement(count, bytes);
313
+ }
314
+ const latency = this.latencyHistogram.clone();
315
+ latency.merge(other.latencyHistogram);
316
+ return projectNativeReportHistoryMeasurement(count, bytes, latency);
317
+ }
294
318
  histogramSnapshot() {
295
319
  return {
296
320
  count: this.count,
@@ -304,6 +328,24 @@ class MeasurementAccumulator {
304
328
  };
305
329
  }
306
330
  }
331
+ function projectNativeReportHistoryMeasurement(count, bytes, latency) {
332
+ const measurement = {
333
+ count,
334
+ bytes,
335
+ approximate: latency?.maxRelativeError !== undefined
336
+ && latency.maxRelativeError > 0
337
+ };
338
+ if (!latency || latency.count === 0n) {
339
+ return measurement;
340
+ }
341
+ return {
342
+ ...measurement,
343
+ percent50Ms: Number(latency.percentile(0.5)) / 1000,
344
+ percent75Ms: Number(latency.percentile(0.75)) / 1000,
345
+ percent95Ms: Number(latency.percentile(0.95)) / 1000,
346
+ percent99Ms: Number(latency.percentile(0.99)) / 1000
347
+ };
348
+ }
307
349
  function buildHistogramMeasurement(snapshot, allRequestCount, durationMs) {
308
350
  const count = snapshot.count;
309
351
  const totalDurationMs = Math.max(durationMs, 0);
@@ -642,6 +684,21 @@ class ScenarioStatsAccumulator {
642
684
  step.record(reply, observedLatencyMs);
643
685
  return step.sortIndex;
644
686
  }
687
+ /**
688
+ * Captures the bounded, private history view without building scenario
689
+ * steps, status rows, plugins, aliases, or any other public result shape.
690
+ */
691
+ buildReportHistorySnapshot() {
692
+ const ok = this.ok.buildReportHistoryMeasurement();
693
+ const failed = this.fail.buildReportHistoryMeasurement();
694
+ return {
695
+ scenarioName: this.scenarioName,
696
+ sortIndex: this.sortIndex,
697
+ all: this.ok.buildCombinedReportHistoryMeasurement(this.fail),
698
+ ...(ok.count > 0 ? { ok } : {}),
699
+ ...(failed.count > 0 ? { failed } : {})
700
+ };
701
+ }
645
702
  /**
646
703
  * Builds the configured payload or helper object.
647
704
  * Use this when all builder inputs are ready to be materialized.
@@ -3509,6 +3566,58 @@ class LoadStrikeRunner {
3509
3566
  licenseSession = await licenseClient.acquireLicenseLease(licensePayload);
3510
3567
  const loggerSetup = createLoggerSetup(this.options.loggerConfig, this.options.minimumLogLevel, this.options, testInfo, nodeInfo);
3511
3568
  const runLogger = loggerSetup.logger;
3569
+ const htmlReportHistoryRequested = (this.options.reportsEnabled ?? true)
3570
+ && normalizeReportFormats(this.options.reportFormats ?? ["html", "txt", "csv", "md"]).includes("html");
3571
+ const distributedReportHistory = isDistributedReportHistoryExecution(clusterMode, this.options);
3572
+ let localReportInput = htmlReportHistoryRequested
3573
+ && distributedReportHistory
3574
+ ? (0, local_report_input_js_1.distributedLocalReportInput)()
3575
+ : (0, local_report_input_js_1.emptyLocalReportInput)();
3576
+ let reportHistoryCollector;
3577
+ let reportHistoryLifecycle;
3578
+ if (htmlReportHistoryRequested && !distributedReportHistory) {
3579
+ const warnReportHistoryFailure = (category, error) => {
3580
+ const exceptionClasses = error === undefined
3581
+ ? ""
3582
+ : `; exception_classes=${(0, report_history_js_1.sanitizedExceptionClassChain)(error)}`;
3583
+ try {
3584
+ runLogger.warn(`LoadStrike report history unavailable: ${category}${exceptionClasses}.`);
3585
+ }
3586
+ catch {
3587
+ // Report-only diagnostics are best-effort.
3588
+ }
3589
+ };
3590
+ reportHistoryCollector = new report_history_js_1.ReportHistoryCollector({
3591
+ scenarioCount: selectedScenarios.length,
3592
+ onCaptureError: (error) => warnReportHistoryFailure("capture_failure", error)
3593
+ });
3594
+ if (!reportHistoryCollector.available) {
3595
+ warnReportHistoryFailure("budget_pressure");
3596
+ }
3597
+ const snapshotFactory = () => Array.from(scenarioAccumulators.values())
3598
+ .map((value) => value.buildReportHistorySnapshot())
3599
+ .sort((left, right) => (left.sortIndex ?? 0) - (right.sortIndex ?? 0)
3600
+ || left.scenarioName.localeCompare(right.scenarioName));
3601
+ const reportHistoryWorker = new report_history_js_1.ReportHistoryWorker({
3602
+ collector: reportHistoryCollector,
3603
+ cadenceSeconds: resolvedReportHistoryCadenceSeconds(this.options.reportingIntervalSeconds ?? 5),
3604
+ snapshotFactory,
3605
+ onFailure: (category, exceptionClasses) => {
3606
+ try {
3607
+ runLogger.warn(`LoadStrike report history unavailable: ${category}; exception_classes=${exceptionClasses}.`);
3608
+ }
3609
+ catch {
3610
+ // Report-only diagnostics are best-effort.
3611
+ }
3612
+ }
3613
+ });
3614
+ reportHistoryLifecycle = new report_history_js_1.ReportHistoryLifecycleCoordinator({
3615
+ scenarioCount: selectedScenarios.length,
3616
+ start: () => reportHistoryWorker.start(),
3617
+ stopAndFinalize: () => reportHistoryWorker.stopAndFinalize(),
3618
+ stopWithoutFinalizing: () => reportHistoryWorker.stopWithoutFinalizing()
3619
+ });
3620
+ }
3512
3621
  const scenarioStartInfos = selectedScenarios.map((scenario, index) => {
3513
3622
  const startInfo = {
3514
3623
  scenarioName: scenario.name,
@@ -3618,7 +3727,7 @@ class LoadStrikeRunner {
3618
3727
  realtimeInFlight = false;
3619
3728
  }
3620
3729
  };
3621
- const reportingIntervalMs = Math.max(Math.trunc((this.options.reportingIntervalSeconds ?? 5) * 1000), 1);
3730
+ const reportingIntervalMs = boundedReportingIntervalMs(this.options.reportingIntervalSeconds ?? 5);
3622
3731
  const triggerRealtimeSnapshot = () => {
3623
3732
  if (realtimeCurrent) {
3624
3733
  return;
@@ -3705,6 +3814,7 @@ class LoadStrikeRunner {
3705
3814
  iterationObservationRunId,
3706
3815
  iterationObservationResultOwnerId,
3707
3816
  iterationObservationProcessGroup,
3817
+ reportHistoryLifecycle,
3708
3818
  executeScenarioInvocation: (targetScenario, context, operation) => this.executeScenarioInvocation(targetScenario, context, operation),
3709
3819
  invokeBeforeScenario: (runtimePolicies, scenarioName) => this.invokeBeforeScenario(runtimePolicies, scenarioName, policyErrors, runtimePolicyErrorMode),
3710
3820
  invokeAfterScenario: (runtimePolicies, scenarioName, stats) => this.invokeAfterScenario(runtimePolicies, scenarioName, stats, policyErrors, runtimePolicyErrorMode),
@@ -3792,9 +3902,14 @@ class LoadStrikeRunner {
3792
3902
  ...iterationObservationReporter.buildWarnings()
3793
3903
  ];
3794
3904
  result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
3905
+ if (reportHistoryCollector) {
3906
+ localReportInput = {
3907
+ history: reportHistoryCollector.toProjection()
3908
+ };
3909
+ }
3795
3910
  const finalizedResult = attachRunResultAliases(result);
3796
3911
  finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
3797
- finalizedResult.reportFiles = this.writeReports(finalizedResult);
3912
+ finalizedResult.reportFiles = this.writeReports(finalizedResult, localReportInput);
3798
3913
  finalizedResult.disabledSinks = sinkStates.filter((x) => x.disabled).map((x) => x.name);
3799
3914
  await this.emitRunResult(sinkStates, finalizedResult, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
3800
3915
  await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
@@ -3805,6 +3920,7 @@ class LoadStrikeRunner {
3805
3920
  return finalizedResult;
3806
3921
  }
3807
3922
  finally {
3923
+ reportHistoryLifecycle?.stopWithoutFinalizing();
3808
3924
  await stopRealtimeReporting();
3809
3925
  if (!iterationObservationsFinalized) {
3810
3926
  await iterationObservationReporter.sealAndDrain().catch(() => { });
@@ -4282,7 +4398,7 @@ class LoadStrikeRunner {
4282
4398
  message: String(error ?? "runtime policy callback failed")
4283
4399
  });
4284
4400
  }
4285
- writeReports(result) {
4401
+ writeReports(result, localReportInput = (0, local_report_input_js_1.emptyLocalReportInput)()) {
4286
4402
  const reportsEnabled = this.options.reportsEnabled ?? true;
4287
4403
  if (!reportsEnabled) {
4288
4404
  return [];
@@ -4307,7 +4423,7 @@ class LoadStrikeRunner {
4307
4423
  (0, node_fs_1.writeFileSync)(path, (0, reporting_js_1.buildDotnetMarkdownReport)(nodeStats), "utf8");
4308
4424
  }
4309
4425
  else if (format === "html") {
4310
- (0, node_fs_1.writeFileSync)(path, (0, reporting_js_1.buildDotnetHtmlReport)(nodeStats), "utf8");
4426
+ (0, node_fs_1.writeFileSync)(path, (0, reporting_js_1.buildDotnetHtmlReport)(nodeStats, localReportInput), "utf8");
4311
4427
  }
4312
4428
  written.push(path);
4313
4429
  }
@@ -4341,6 +4457,19 @@ function hasPluginRows(value) {
4341
4457
  }
4342
4458
  return value.tables.some((table) => Array.isArray(table.rows) && table.rows.length > 0);
4343
4459
  }
4460
+ function boundedReportingIntervalMs(seconds) {
4461
+ if (!Number.isFinite(seconds) || seconds <= 0) {
4462
+ return 1;
4463
+ }
4464
+ const milliseconds = seconds * 1000;
4465
+ if (!Number.isFinite(milliseconds) || milliseconds >= 2147483647) {
4466
+ return 2147483647;
4467
+ }
4468
+ return Math.max(Math.trunc(milliseconds), 1);
4469
+ }
4470
+ function resolvedReportHistoryCadenceSeconds(seconds) {
4471
+ return boundedReportingIntervalMs(seconds) / 1000;
4472
+ }
4344
4473
  async function executeV2FixedArrivals(args) {
4345
4474
  const { rate, intervalNs, totalArrivals, budget, cancellationToken, shouldStopNow, nextInstanceInfo, runBombingInvocation, logger, scenarioName, deadlineOffsetsNs, tolerancesNs, ownedOrdinals, shardIndex = 0, shardCount = 1, telemetry, segment } = args;
4346
4475
  const segmentStartNs = process.hrtime.bigint();
@@ -4477,7 +4606,15 @@ function maxBigInt(left, right) {
4477
4606
  return left > right ? left : right;
4478
4607
  }
4479
4608
  async function executeScenarioRuntime(args) {
4480
- 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;
4609
+ 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, reportHistoryLifecycle, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
4610
+ let reportHistoryExecutionEnded = false;
4611
+ const endReportHistoryExecution = async () => {
4612
+ if (reportHistoryExecutionEnded) {
4613
+ return;
4614
+ }
4615
+ reportHistoryExecutionEnded = true;
4616
+ await reportHistoryLifecycle?.scenarioExecutionEnded();
4617
+ };
4481
4618
  const scenarioStartedMs = Date.now();
4482
4619
  const scenarioContextData = {};
4483
4620
  const registeredMetrics = [];
@@ -5298,6 +5435,7 @@ async function executeScenarioRuntime(args) {
5298
5435
  })));
5299
5436
  await invokeBeforeScenario(policies, scenario.name);
5300
5437
  await runWarmUpAsync();
5438
+ reportHistoryLifecycle?.scenarioBombingStarted();
5301
5439
  accumulator.setCurrentOperation("Bombing");
5302
5440
  const simulations = scenario.getSimulations();
5303
5441
  if (!simulations.length) {
@@ -5331,6 +5469,7 @@ async function executeScenarioRuntime(args) {
5331
5469
  }
5332
5470
  }
5333
5471
  accumulator.setCurrentOperation(stopScenario || stopTestState.value ? "Stop" : "Complete");
5472
+ await endReportHistoryExecution();
5334
5473
  await invokeAfterScenario(policies, scenario.name, runtime);
5335
5474
  }
5336
5475
  catch (error) {
@@ -5347,6 +5486,7 @@ async function executeScenarioRuntime(args) {
5347
5486
  }
5348
5487
  }
5349
5488
  finally {
5489
+ await endReportHistoryExecution();
5350
5490
  scenarioDurationsMs.set(scenario.name, Math.max(Date.now() - scenarioStartedMs, 0));
5351
5491
  try {
5352
5492
  await scenario.invokeClean({
@@ -7280,6 +7420,11 @@ function resolveClusterExecutionMode(options) {
7280
7420
  }
7281
7421
  return "single";
7282
7422
  }
7423
+ function isDistributedReportHistoryExecution(clusterMode, options) {
7424
+ return clusterMode !== "single"
7425
+ || options.loadEngineV2SegmentLifecycleOverride !== undefined
7426
+ || Math.max(Math.trunc(options.clusterShardCount ?? 1), 1) > 1;
7427
+ }
7283
7428
  function buildEmptyNodeStats(args) {
7284
7429
  return attachNodeStatsAliases({
7285
7430
  startedUtc: args.startedUtc,
@@ -11111,6 +11256,7 @@ function buildGroupedCorrelationRows(rows) {
11111
11256
  LatencyMinMs: latencySamples.length ? formatOptionalLatency(latencySamples[0]) : "",
11112
11257
  LatencyMeanMs: latencySamples.length ? formatOptionalLatency(latencySamples.reduce((sum, value) => sum + value, 0) / latencySamples.length) : "",
11113
11258
  LatencyP50Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.5)) : "",
11259
+ LatencyP75Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.75)) : "",
11114
11260
  LatencyP80Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.8)) : "",
11115
11261
  LatencyP85Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.85)) : "",
11116
11262
  LatencyP90Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.9)) : "",
@@ -11323,6 +11469,8 @@ exports.__loadstrikeTestExports = {
11323
11469
  buildThresholdCheckExpression,
11324
11470
  buildTrackingLeaseKey,
11325
11471
  buildTrackingRunNamespace,
11472
+ boundedReportingIntervalMs,
11473
+ resolvedReportHistoryCadenceSeconds,
11326
11474
  clusterNodeResultToNodeStats,
11327
11475
  combineAbortSignals,
11328
11476
  computeScenarioRequestCount,
@@ -11342,6 +11490,7 @@ exports.__loadstrikeTestExports = {
11342
11490
  formatUtcReportTimestamp,
11343
11491
  hasPluginRows,
11344
11492
  inferRuntimeLegacyHttpResponseSource,
11493
+ isDistributedReportHistoryExecution,
11345
11494
  isComparisonFailed,
11346
11495
  loadJsonObject,
11347
11496
  logLevelOrder,
@@ -0,0 +1,17 @@
1
+ export function emptyLocalReportInput() {
2
+ return {
3
+ history: {
4
+ status: "available",
5
+ points: []
6
+ }
7
+ };
8
+ }
9
+ export function distributedLocalReportInput() {
10
+ return {
11
+ history: {
12
+ status: "unavailable",
13
+ reasonCategory: "distributed_temporal_aggregation_unavailable",
14
+ points: []
15
+ }
16
+ };
17
+ }