@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.
package/dist/cjs/local.js CHANGED
@@ -46,7 +46,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
46
46
  var __importDefault = (this && this.__importDefault) || function (mod) {
47
47
  return (mod && mod.__esModule) ? mod : { "default": mod };
48
48
  };
49
- var _LoadStrikeLocalClient_licensingApiBaseUrl, _LoadStrikeLocalClient_signingKeyCache;
49
+ var _LoadStrikeLocalClient_licensingApiBaseUrl, _LoadStrikeLocalClient_signingKeyCache, _LoadStrikeLocalClient_heartbeatDrains;
50
50
  Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.__loadstrikeTestExports = exports.LoadStrikeLocalClient = void 0;
52
52
  const node_os_1 = __importDefault(require("node:os"));
@@ -109,6 +109,7 @@ class LoadStrikeLocalClient {
109
109
  constructor(options = {}) {
110
110
  _LoadStrikeLocalClient_licensingApiBaseUrl.set(this, void 0);
111
111
  _LoadStrikeLocalClient_signingKeyCache.set(this, new Map());
112
+ _LoadStrikeLocalClient_heartbeatDrains.set(this, new WeakMap());
112
113
  assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
113
114
  __classPrivateFieldSet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, resolveLicensingApiBaseUrl(), "f");
114
115
  this.licenseValidationTimeoutMs = normalizeTimeoutMs(options.licenseValidationTimeoutMs);
@@ -228,28 +229,59 @@ class LoadStrikeLocalClient {
228
229
  }
229
230
  await this.verifySignedRunToken(runToken, request, requestedFeatures, runnerKey, sessionId, computedDeviceHash);
230
231
  const heartbeatIntervalSeconds = Math.max(asInt(pickValue(json, "HeartbeatIntervalSeconds", "heartbeatIntervalSeconds")), 1);
231
- const heartbeatTimer = setInterval(() => {
232
- void this.sendRunTokenHeartbeat({
233
- runToken,
234
- sessionId,
235
- deviceHash: computedDeviceHash,
236
- machineName,
237
- environmentClassification
238
- }).catch(() => {
239
- // Best-effort heartbeat: server-side lease expiration is authoritative.
240
- });
241
- }, heartbeatIntervalSeconds * 1000);
242
- if (typeof heartbeatTimer.unref === "function") {
243
- heartbeatTimer.unref();
244
- }
245
- return {
232
+ const session = {
246
233
  runToken,
247
234
  sessionId,
248
235
  deviceHash: computedDeviceHash,
249
236
  machineName,
250
- environmentClassification,
251
- heartbeatTimer
237
+ environmentClassification
238
+ };
239
+ let heartbeatInFlight = false;
240
+ const runHeartbeat = async () => {
241
+ heartbeatInFlight = true;
242
+ try {
243
+ const currentRunToken = stringOrDefault(session.runToken, "").trim();
244
+ if (!currentRunToken) {
245
+ return;
246
+ }
247
+ const refreshedRunToken = await this.sendRunTokenHeartbeat({
248
+ runToken: currentRunToken,
249
+ sessionId,
250
+ deviceHash: computedDeviceHash,
251
+ machineName,
252
+ environmentClassification
253
+ });
254
+ if (!refreshedRunToken) {
255
+ return;
256
+ }
257
+ try {
258
+ await this.verifySignedRunToken(refreshedRunToken, request, requestedFeatures, runnerKey, sessionId, computedDeviceHash);
259
+ }
260
+ catch {
261
+ return;
262
+ }
263
+ session.runToken = refreshedRunToken;
264
+ }
265
+ catch {
266
+ // Best-effort heartbeat: server-side lease expiration is authoritative.
267
+ }
268
+ finally {
269
+ heartbeatInFlight = false;
270
+ }
252
271
  };
272
+ let currentHeartbeat = Promise.resolve();
273
+ const heartbeatTimer = setInterval(() => {
274
+ if (!heartbeatInFlight) {
275
+ currentHeartbeat = runHeartbeat();
276
+ }
277
+ return currentHeartbeat;
278
+ }, heartbeatIntervalSeconds * 1000);
279
+ if (typeof heartbeatTimer.unref === "function") {
280
+ heartbeatTimer.unref();
281
+ }
282
+ session.heartbeatTimer = heartbeatTimer;
283
+ __classPrivateFieldGet(this, _LoadStrikeLocalClient_heartbeatDrains, "f").set(session, () => currentHeartbeat);
284
+ return session;
253
285
  }
254
286
  finally {
255
287
  clearTimeout(timer);
@@ -433,18 +465,35 @@ class LoadStrikeLocalClient {
433
465
  const timer = controller
434
466
  ? setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs)
435
467
  : null;
436
- const { response } = await this.postLicensingRequest("/api/v1/licenses/heartbeat", heartbeatPayload, signal ?? controller.signal);
468
+ const { response, json } = await this.postLicensingRequest("/api/v1/licenses/heartbeat", heartbeatPayload, signal ?? controller.signal);
437
469
  if (timer) {
438
470
  clearTimeout(timer);
439
471
  }
440
472
  if (!response.ok) {
441
473
  throw new Error(`Runner key validation denied. DenialCode=run_token_heartbeat_failed, Message=Run token heartbeat failed with status ${response.status}.`);
442
474
  }
475
+ if (pickValue(json ?? {}, "IsValid", "isValid") !== true) {
476
+ return undefined;
477
+ }
478
+ const refreshedRunToken = stringOrDefault(pickValue(json ?? {}, "RunToken", "runToken"), "").trim();
479
+ return refreshedRunToken || undefined;
443
480
  }
444
481
  async stopLicenseLeaseIfRequired(session, _request) {
445
482
  if (session.heartbeatTimer) {
446
483
  clearInterval(session.heartbeatTimer);
447
484
  }
485
+ const heartbeatDrain = __classPrivateFieldGet(this, _LoadStrikeLocalClient_heartbeatDrains, "f").get(session);
486
+ if (heartbeatDrain) {
487
+ try {
488
+ await heartbeatDrain();
489
+ }
490
+ catch {
491
+ // Heartbeats are best-effort and retain the last fully verified token.
492
+ }
493
+ finally {
494
+ __classPrivateFieldGet(this, _LoadStrikeLocalClient_heartbeatDrains, "f").delete(session);
495
+ }
496
+ }
448
497
  if (!session.runToken) {
449
498
  return;
450
499
  }
@@ -503,7 +552,7 @@ class LoadStrikeLocalClient {
503
552
  }
504
553
  }
505
554
  exports.LoadStrikeLocalClient = LoadStrikeLocalClient;
506
- _LoadStrikeLocalClient_licensingApiBaseUrl = new WeakMap(), _LoadStrikeLocalClient_signingKeyCache = new WeakMap();
555
+ _LoadStrikeLocalClient_licensingApiBaseUrl = new WeakMap(), _LoadStrikeLocalClient_signingKeyCache = new WeakMap(), _LoadStrikeLocalClient_heartbeatDrains = new WeakMap();
507
556
  function assertNoDisableLicenseEnforcementOption(value, source) {
508
557
  if (value == null || typeof value !== "object" || Array.isArray(value)) {
509
558
  return;
@@ -1867,6 +1916,10 @@ function isRuntimeReportingSink(value) {
1867
1916
  "SaveRealtimeMetrics",
1868
1917
  "saveRunResult",
1869
1918
  "SaveRunResult",
1919
+ "saveIterationBatch",
1920
+ "SaveIterationBatch",
1921
+ "completeIterationObservationStream",
1922
+ "CompleteIterationObservationStream",
1870
1923
  "stop",
1871
1924
  "Stop"
1872
1925
  ].some((name) => typeof record[name] === "function");
@@ -139,6 +139,26 @@ function asFloat(value) {
139
139
  const parsed = Number.parseFloat(asString(value));
140
140
  return Number.isFinite(parsed) ? parsed : 0;
141
141
  }
142
+ function combinedMeasurement(source) {
143
+ const all = reportValue(source, "allMeasurement", "AllMeasurement");
144
+ if (all && typeof all === "object" && !Array.isArray(all)) {
145
+ return all;
146
+ }
147
+ const ok = reportObject(source, "ok", "Ok");
148
+ const fail = reportObject(source, "fail", "Fail");
149
+ const okCount = asInt(reportValue(reportObject(ok, "request", "Request"), "count", "Count"));
150
+ const failCount = asInt(reportValue(reportObject(fail, "request", "Request"), "count", "Count"));
151
+ if (okCount === 0) {
152
+ return fail;
153
+ }
154
+ return failCount === 0 ? ok : undefined;
155
+ }
156
+ function formatCombinedLatency(source, ...keys) {
157
+ const measurement = combinedMeasurement(source);
158
+ return measurement
159
+ ? formatReportNumber(reportValue(reportObject(measurement, "latency", "Latency"), ...keys))
160
+ : "n/a";
161
+ }
142
162
  function asBool(value) {
143
163
  if (typeof value === "boolean") {
144
164
  return value;
@@ -754,8 +774,8 @@ function buildDotnetScenarioRows(nodeStats) {
754
774
  FAIL: reportFailCountValue(scenario),
755
775
  Duration: formatDotnetTimeSpan(reportDurationValue(scenario)),
756
776
  RPS: formatReportNumber(reportDurationSeconds(reportDurationValue(scenario)) <= 0 ? 0 : requestCount / reportDurationSeconds(reportDurationValue(scenario))),
757
- LatencyP95Ms: formatReportNumber(Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent95", "Percent95")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent95", "Percent95")))),
758
- LatencyP99Ms: formatReportNumber(Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent99", "Percent99")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent99", "Percent99")))),
777
+ LatencyP95Ms: formatCombinedLatency(scenario, "percent95", "Percent95"),
778
+ LatencyP99Ms: formatCombinedLatency(scenario, "percent99", "Percent99"),
759
779
  CurrentOperation: asString(reportValue(scenario, "currentOperation", "CurrentOperation"))
760
780
  };
761
781
  });
@@ -772,8 +792,8 @@ function buildDotnetStepRows(nodeStats) {
772
792
  FAIL: asInt(reportValue(reportObject(reportObject(step, "fail", "Fail"), "request", "Request"), "count", "Count")),
773
793
  OK_RPS: formatReportNumber(reportValue(reportObject(reportObject(step, "ok", "Ok"), "request", "Request"), "rps", "RPS")),
774
794
  FAIL_RPS: formatReportNumber(reportValue(reportObject(reportObject(step, "fail", "Fail"), "request", "Request"), "rps", "RPS")),
775
- LatencyMeanMs: formatReportNumber(Math.max(asFloat(reportValue(reportObject(reportObject(step, "ok", "Ok"), "latency", "Latency"), "meanMs", "MeanMs")), asFloat(reportValue(reportObject(reportObject(step, "fail", "Fail"), "latency", "Latency"), "meanMs", "MeanMs")))),
776
- P95LatencyMs: formatReportNumber(Math.max(asFloat(reportValue(reportObject(reportObject(step, "ok", "Ok"), "latency", "Latency"), "percent95", "Percent95")), asFloat(reportValue(reportObject(reportObject(step, "fail", "Fail"), "latency", "Latency"), "percent95", "Percent95"))))
795
+ LatencyMeanMs: formatCombinedLatency(step, "meanMs", "MeanMs"),
796
+ P95LatencyMs: formatCombinedLatency(step, "percent95", "Percent95")
777
797
  });
778
798
  }
779
799
  }
@@ -783,6 +803,10 @@ function buildDotnetScenarioMeasurementRows(nodeStats) {
783
803
  const rows = [];
784
804
  for (const scenario of reportScenarios(nodeStats)) {
785
805
  const scenarioName = asString(reportValue(scenario, "scenarioName", "ScenarioName"));
806
+ const allMeasurement = reportValue(scenario, "allMeasurement", "AllMeasurement");
807
+ if (allMeasurement && typeof allMeasurement === "object" && !Array.isArray(allMeasurement)) {
808
+ pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "ALL", allMeasurement);
809
+ }
786
810
  pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "OK", reportObject(scenario, "ok", "Ok"));
787
811
  pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "FAIL", reportObject(scenario, "fail", "Fail"));
788
812
  }
@@ -794,6 +818,10 @@ function buildDotnetStepMeasurementRows(nodeStats) {
794
818
  const scenarioName = asString(reportValue(scenario, "scenarioName", "ScenarioName"));
795
819
  for (const step of reportSteps(scenario)) {
796
820
  const stepName = asString(reportValue(step, "stepName", "StepName"));
821
+ const allMeasurement = reportValue(step, "allMeasurement", "AllMeasurement");
822
+ if (allMeasurement && typeof allMeasurement === "object" && !Array.isArray(allMeasurement)) {
823
+ pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "ALL", allMeasurement);
824
+ }
797
825
  pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "OK", reportObject(step, "ok", "Ok"));
798
826
  pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "FAIL", reportObject(step, "fail", "Fail"));
799
827
  }
@@ -874,6 +902,9 @@ function buildDotnetStatusCodeClassChart(scenarios) {
874
902
  }
875
903
  function buildDotnetChartData(nodeStats) {
876
904
  const scenarios = reportScenarios(nodeStats);
905
+ const combinedScenarios = scenarios
906
+ .map((scenario) => ({ scenario, measurement: combinedMeasurement(scenario) }))
907
+ .filter((item) => item.measurement !== undefined);
877
908
  return {
878
909
  overallOutcome: [
879
910
  { label: "OK", value: reportTotalOkCount(nodeStats, scenarios), color: "#18a957" },
@@ -884,9 +915,9 @@ function buildDotnetChartData(nodeStats) {
884
915
  value: reportRequestCountValue(scenario),
885
916
  color: "#3b82f6"
886
917
  })),
887
- scenarioP95Latency: scenarios.map((scenario) => ({
918
+ scenarioP95Latency: combinedScenarios.map(({ scenario, measurement }) => ({
888
919
  label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
889
- value: Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent95", "Percent95")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent95", "Percent95"))),
920
+ value: asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95")),
890
921
  color: "#8b5cf6"
891
922
  })),
892
923
  scenarioRps: scenarios.map((scenario) => ({
@@ -910,12 +941,12 @@ function buildDotnetChartData(nodeStats) {
910
941
  })),
911
942
  statusCodeClasses: buildDotnetStatusCodeClassChart(scenarios),
912
943
  scenarioLatencyTrend: {
913
- labels: scenarios.map((scenario) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
944
+ labels: combinedScenarios.map(({ scenario }) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
914
945
  series: [
915
- { name: "P50", color: "#38bdf8", values: scenarios.map((scenario) => Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent50", "Percent50")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent50", "Percent50")))) },
916
- { name: "P75", color: "#22c55e", values: scenarios.map((scenario) => Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent75", "Percent75")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent75", "Percent75")))) },
917
- { name: "P95", color: "#f59e0b", values: scenarios.map((scenario) => Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent95", "Percent95")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent95", "Percent95")))) },
918
- { name: "P99", color: "#f43f5e", values: scenarios.map((scenario) => Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent99", "Percent99")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent99", "Percent99")))) }
946
+ { name: "P50", color: "#38bdf8", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent50", "Percent50"))) },
947
+ { name: "P75", color: "#22c55e", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent75", "Percent75"))) },
948
+ { name: "P95", color: "#f59e0b", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95"))) },
949
+ { name: "P99", color: "#f43f5e", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent99", "Percent99"))) }
919
950
  ]
920
951
  }
921
952
  };
@@ -1060,6 +1091,82 @@ function buildDotnetThresholdHtml(nodeStats) {
1060
1091
  function buildDotnetMetricHtml(nodeStats) {
1061
1092
  return buildDotnetTableHtml(buildDotnetMetricRows(nodeStats));
1062
1093
  }
1094
+ function buildDotnetGeneratorDeliveryHtml(nodeStats) {
1095
+ const warnings = reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings");
1096
+ const stats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
1097
+ const topLevelSegments = reportArray(nodeStats, "schedulerSegments", "SchedulerSegments");
1098
+ const segments = topLevelSegments.length > 0
1099
+ ? topLevelSegments
1100
+ : reportArray(stats, "segments", "Segments");
1101
+ const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
1102
+ const reportingCompleteValue = reportValue(nodeStats, "reportingComplete", "ReportingComplete");
1103
+ const reportingComplete = reportingCompleteValue == null
1104
+ ? "N/A"
1105
+ : asBool(reportingCompleteValue) ? "Yes" : "No";
1106
+ const parts = [];
1107
+ appendReportLine(parts, "<div class=\"card\">");
1108
+ appendReportLine(parts, "<h2>Generator Delivery</h2>");
1109
+ appendReportLine(parts, "<p>Application failures remain separate from generator and reporting warnings. Dropped arrivals, unavailable worker slots, and raw observation loss are not synthetic SUT errors.</p>");
1110
+ appendReportLine(parts, "<div class=\"card-grid\">");
1111
+ for (const [label, value] of [
1112
+ ["Configured Max In Flight", asInt(reportValue(stats, "configuredMaxInFlight", "ConfiguredMaxInFlight"))],
1113
+ ["Observed Max In Flight", asInt(reportValue(stats, "maxInFlightObserved", "MaxInFlightObserved"))],
1114
+ ["Current In Flight", asInt(reportValue(stats, "currentInFlight", "CurrentInFlight"))],
1115
+ ["Observation Captured", asString(reportValue(observationStats, "capturedCount64", "CapturedCount64")) || "0"],
1116
+ ["Observation Delivered", asString(reportValue(observationStats, "deliveredCount64", "DeliveredCount64")) || "0"],
1117
+ ["Observation Buffer Drops", asString(reportValue(observationStats, "droppedBufferCount64", "DroppedBufferCount64")) || "0"],
1118
+ ["Observation Sink Drops", asString(reportValue(observationStats, "droppedSinkCount64", "DroppedSinkCount64")) || "0"],
1119
+ ["Reporting Complete", reportingComplete],
1120
+ ["Warning Groups", warnings.length]
1121
+ ]) {
1122
+ appendReportLine(parts, `<div class="stat-card"><div class="stat-label">${escapeHtml(label)}</div><div class="stat-value">${escapeHtml(value)}</div></div>`);
1123
+ }
1124
+ appendReportLine(parts, "</div></div>");
1125
+ if (warnings.length) {
1126
+ appendReportLine(parts, "<div class=\"card\"><h2>Generator Warnings</h2>");
1127
+ parts.push(buildDotnetTableHtml(warnings, false));
1128
+ appendReportLine(parts, "</div>");
1129
+ }
1130
+ if (segments.length) {
1131
+ const rows = segments.map((segment) => ({
1132
+ Scenario: asString(reportValue(segment, "scenarioName", "ScenarioName")),
1133
+ Simulation: asString(reportValue(segment, "kind", "Kind")),
1134
+ Shard: `${asInt(reportValue(segment, "shardIndex", "ShardIndex"))}/${Math.max(asInt(reportValue(segment, "shardCount", "ShardCount")), 1)}`,
1135
+ Planned: asString(reportValue(segment, "plannedIterations64", "PlannedIterations64")),
1136
+ Due: asString(reportValue(segment, "dueIterations64", "DueIterations64")),
1137
+ Started: asString(reportValue(segment, "startedIterations64", "StartedIterations64")),
1138
+ Completed: asString(reportValue(segment, "completedIterations64", "CompletedIterations64")),
1139
+ Dropped: asString(reportValue(segment, "droppedIterations64", "DroppedIterations64")),
1140
+ Unreached: asString(reportValue(segment, "unreachedIterations64", "UnreachedIterations64")),
1141
+ "Unavailable Workers": asString(reportValue(segment, "unavailableWorkerSlots64", "UnavailableWorkerSlots64")),
1142
+ "Delivery %": formatReportNumber(reportValue(segment, "deliveryPercent", "DeliveryPercent")),
1143
+ "Accounting Complete": Boolean(reportValue(segment, "accountingComplete", "AccountingComplete"))
1144
+ }));
1145
+ appendReportLine(parts, "<div class=\"card\"><h2>Scheduler Segments</h2>");
1146
+ parts.push(buildDotnetTableHtml(rows, false));
1147
+ appendReportLine(parts, "</div>");
1148
+ }
1149
+ return parts.join("");
1150
+ }
1151
+ function hasDotnetGeneratorDeliveryData(nodeStats) {
1152
+ const schedulerStats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
1153
+ const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
1154
+ const hasNonZeroDecimal = (value) => {
1155
+ const text = asString(value);
1156
+ return text.trim().length > 0 && text !== "0";
1157
+ };
1158
+ return reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings").length > 0
1159
+ || reportArray(nodeStats, "schedulerSegments", "SchedulerSegments").length > 0
1160
+ || reportArray(schedulerStats, "segments", "Segments").length > 0
1161
+ || asInt(reportValue(schedulerStats, "configuredMaxInFlight", "ConfiguredMaxInFlight")) > 0
1162
+ || asInt(reportValue(schedulerStats, "maxInFlightObserved", "MaxInFlightObserved")) > 0
1163
+ || asInt(reportValue(schedulerStats, "currentInFlight", "CurrentInFlight")) > 0
1164
+ || reportValue(nodeStats, "reportingComplete", "ReportingComplete") === false
1165
+ || hasNonZeroDecimal(reportValue(observationStats, "capturedCount64", "CapturedCount64"))
1166
+ || hasNonZeroDecimal(reportValue(observationStats, "deliveredCount64", "DeliveredCount64"))
1167
+ || hasNonZeroDecimal(reportValue(observationStats, "droppedBufferCount64", "DroppedBufferCount64"))
1168
+ || hasNonZeroDecimal(reportValue(observationStats, "droppedSinkCount64", "DroppedSinkCount64"));
1169
+ }
1063
1170
  function buildDotnetGroupedCorrelationSummaryHtml(rows, groupedChartKey) {
1064
1171
  const parts = [];
1065
1172
  const payloads = buildGroupedCorrelationChartPayloads(rows);
@@ -1136,6 +1243,9 @@ function buildDotnetHtmlTabs(nodeStats) {
1136
1243
  if (metricRows.length) {
1137
1244
  tabs.push(["metrics", "Metrics", buildDotnetTableHtml(metricRows)]);
1138
1245
  }
1246
+ if (hasDotnetGeneratorDeliveryData(nodeStats)) {
1247
+ tabs.push(["generator-delivery", "Generator Delivery", buildDotnetGeneratorDeliveryHtml(nodeStats)]);
1248
+ }
1139
1249
  for (const plugin of reportArray(nodeStats, "pluginsData", "PluginsData")) {
1140
1250
  const pluginName = asString(reportValue(plugin, "pluginName", "PluginName"));
1141
1251
  const hints = buildDotnetPluginHints(plugin);
@@ -1379,7 +1489,7 @@ const reportLogo=document.querySelector('[data-report-logo]');
1379
1489
  function applyReportTheme(theme){const normalized=theme==='dark'?'dark':'light';document.body.setAttribute('data-theme',normalized);if(reportLogo){const lightLogo=reportLogo.dataset.logoLight||reportLogo.getAttribute('src');const darkLogo=reportLogo.dataset.logoDark||reportLogo.getAttribute('src');reportLogo.setAttribute('src',normalized==='dark'?darkLogo:lightLogo);}if(reportThemeToggle){const darkActive=normalized==='dark';const nextLabel=darkActive?'light':'dark';reportThemeToggle.innerHTML=darkActive?'&#x2600;':'&#x263E;';reportThemeToggle.setAttribute('aria-pressed',darkActive?'true':'false');reportThemeToggle.setAttribute('aria-label','Switch to '+nextLabel+' theme');reportThemeToggle.setAttribute('title','Switch to '+nextLabel+' theme');}}
1380
1490
  function show(id){btns.forEach(b=>b.classList.toggle('active',b.dataset.tab===id));tabSections.forEach(t=>t.classList.toggle('active',t.id===id));}
1381
1491
  function formatMetric(v){if(!Number.isFinite(v))return '0';return Math.abs(v)>=100?v.toFixed(0):v.toFixed(2);}
1382
- function setupCanvas(canvas){const dpr=window.devicePixelRatio||1;const w=Math.max(320,canvas.clientWidth||320);const h=Math.max(220,canvas.clientHeight||220);canvas.width=Math.floor(w*dpr);canvas.height=Math.floor(h*dpr);const ctx=canvas.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);return {ctx,w,h};}
1492
+ function setupCanvas(canvas){const dpr=window.devicePixelRatio||1;const rect=canvas.getBoundingClientRect();const w=Math.max(1,Math.round(rect.width||canvas.clientWidth||320));const h=Math.max(1,Math.round(rect.height||canvas.clientHeight||220));canvas.width=Math.max(1,Math.round(w*dpr));canvas.height=Math.max(1,Math.round(h*dpr));const ctx=canvas.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);return {ctx,w,h};}
1383
1493
  function drawNoData(ctx,w,h,msg){ctx.fillStyle='#9fb0c3';ctx.font='13px Segoe UI';ctx.textAlign='center';ctx.fillText(msg,w/2,h/2);}
1384
1494
  function drawBar(canvasId,points){const canvas=document.getElementById(canvasId);if(!canvas)return;const c=setupCanvas(canvas);const ctx=c.ctx,w=c.w,h=c.h;ctx.clearRect(0,0,w,h);if(!points||points.length===0){drawNoData(ctx,w,h,'No data');return;}const left=46,right=14,top=16,bottom=62;const pw=w-left-right;const ph=h-top-bottom;const max=Math.max(...points.map(p=>p.value),1);ctx.strokeStyle='#334155';ctx.lineWidth=1;for(let i=0;i<=4;i++){const y=top+(ph*(i/4));ctx.beginPath();ctx.moveTo(left,y);ctx.lineTo(w-right,y);ctx.stroke();}const slot=pw/points.length;const bar=Math.max(8,slot*0.58);ctx.font='11px Segoe UI';for(let i=0;i<points.length;i++){const p=points[i];const x=left+i*slot+(slot-bar)/2;const bh=(p.value/max)*ph;const y=top+ph-bh;ctx.fillStyle=p.color||'#3b82f6';ctx.fillRect(x,y,bar,bh);ctx.fillStyle='#dbe6f4';ctx.textAlign='center';ctx.fillText(formatMetric(p.value),x+bar/2,Math.max(12,y-4));ctx.save();ctx.translate(x+bar/2,h-bottom+14);ctx.rotate(-0.6);ctx.fillStyle='#b5c2d3';ctx.fillText((p.label||'').slice(0,26),0,0);ctx.restore();}ctx.fillStyle='#b5c2d3';ctx.textAlign='right';for(let i=0;i<=4;i++){const value=max*(1-i/4);const y=top+(ph*(i/4))+4;ctx.fillText(formatMetric(value),left-6,y);}}
1385
1495
  function drawPie(canvasId,points){const canvas=document.getElementById(canvasId);if(!canvas)return;const c=setupCanvas(canvas);const ctx=c.ctx,w=c.w,h=c.h;ctx.clearRect(0,0,w,h);if(!points||points.length===0){drawNoData(ctx,w,h,'No data');return;}const total=points.reduce((s,p)=>s+(p.value||0),0);if(total<=0){drawNoData(ctx,w,h,'No data');return;}const cx=w*0.35,cy=h*0.5,r=Math.min(w,h)*0.28;let angle=-Math.PI/2;for(const p of points){const val=Math.max(0,p.value||0);const delta=(val/total)*Math.PI*2;ctx.beginPath();ctx.moveTo(cx,cy);ctx.arc(cx,cy,r,angle,angle+delta);ctx.closePath();ctx.fillStyle=p.color||'#3b82f6';ctx.fill();angle+=delta;}ctx.fillStyle='#0f172a';ctx.beginPath();ctx.arc(cx,cy,r*0.54,0,Math.PI*2);ctx.fill();ctx.fillStyle='#e5eefc';ctx.font='bold 18px Segoe UI';ctx.textAlign='center';ctx.fillText(total.toString(),cx,cy+6);ctx.font='12px Segoe UI';ctx.fillStyle='#9fb0c3';ctx.fillText('requests',cx,cy+24);ctx.textAlign='left';let y=cy-r+10;for(const p of points){ctx.fillStyle=p.color||'#3b82f6';ctx.fillRect(w*0.64,y-10,12,12);ctx.fillStyle='#e6edf3';ctx.font='12px Segoe UI';const pct=total<=0?0:((p.value/total)*100);ctx.fillText(\`\${p.label}: \${p.value} (\${pct.toFixed(1)}%)\`,w*0.64+18,y);y+=20;}}
@@ -1395,7 +1505,7 @@ function renderAllCharts(){renderCharts();renderUngroupedCorrelationCharts();ren
1395
1505
  const storedReportTheme=(()=>{try{return localStorage.getItem(reportThemeKey);}catch{return null;}})();
1396
1506
  applyReportTheme(storedReportTheme==='dark'?'dark':'light');
1397
1507
  if(reportThemeToggle){reportThemeToggle.addEventListener('click',()=>{const next=document.body.getAttribute('data-theme')==='dark'?'light':'dark';applyReportTheme(next);try{localStorage.setItem(reportThemeKey,next);}catch{}renderAllCharts();});}
1398
- btns.forEach(b=>b.addEventListener('click',()=>show(b.dataset.tab)));
1508
+ btns.forEach(b=>b.addEventListener('click',()=>{show(b.dataset.tab);requestAnimationFrame(renderAllCharts);}));
1399
1509
  if(btns.length>0){show(btns[0].dataset.tab);}renderAllCharts();initPanePan();window.addEventListener('resize',renderAllCharts);
1400
1510
  </script>
1401
1511
  </body>