@loadstrike/loadstrike-sdk 1.0.30201 → 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/esm/local.js CHANGED
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _LoadStrikeLocalClient_licensingApiBaseUrl, _LoadStrikeLocalClient_signingKeyCache;
12
+ var _LoadStrikeLocalClient_licensingApiBaseUrl, _LoadStrikeLocalClient_signingKeyCache, _LoadStrikeLocalClient_heartbeatDrains;
13
13
  import os from "node:os";
14
14
  import * as fs from "node:fs";
15
15
  import * as childProcess from "node:child_process";
@@ -70,6 +70,7 @@ export class LoadStrikeLocalClient {
70
70
  constructor(options = {}) {
71
71
  _LoadStrikeLocalClient_licensingApiBaseUrl.set(this, void 0);
72
72
  _LoadStrikeLocalClient_signingKeyCache.set(this, new Map());
73
+ _LoadStrikeLocalClient_heartbeatDrains.set(this, new WeakMap());
73
74
  assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
74
75
  __classPrivateFieldSet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, resolveLicensingApiBaseUrl(), "f");
75
76
  this.licenseValidationTimeoutMs = normalizeTimeoutMs(options.licenseValidationTimeoutMs);
@@ -189,28 +190,59 @@ export class LoadStrikeLocalClient {
189
190
  }
190
191
  await this.verifySignedRunToken(runToken, request, requestedFeatures, runnerKey, sessionId, computedDeviceHash);
191
192
  const heartbeatIntervalSeconds = Math.max(asInt(pickValue(json, "HeartbeatIntervalSeconds", "heartbeatIntervalSeconds")), 1);
192
- const heartbeatTimer = setInterval(() => {
193
- void this.sendRunTokenHeartbeat({
194
- runToken,
195
- sessionId,
196
- deviceHash: computedDeviceHash,
197
- machineName,
198
- environmentClassification
199
- }).catch(() => {
200
- // Best-effort heartbeat: server-side lease expiration is authoritative.
201
- });
202
- }, heartbeatIntervalSeconds * 1000);
203
- if (typeof heartbeatTimer.unref === "function") {
204
- heartbeatTimer.unref();
205
- }
206
- return {
193
+ const session = {
207
194
  runToken,
208
195
  sessionId,
209
196
  deviceHash: computedDeviceHash,
210
197
  machineName,
211
- environmentClassification,
212
- heartbeatTimer
198
+ environmentClassification
199
+ };
200
+ let heartbeatInFlight = false;
201
+ const runHeartbeat = async () => {
202
+ heartbeatInFlight = true;
203
+ try {
204
+ const currentRunToken = stringOrDefault(session.runToken, "").trim();
205
+ if (!currentRunToken) {
206
+ return;
207
+ }
208
+ const refreshedRunToken = await this.sendRunTokenHeartbeat({
209
+ runToken: currentRunToken,
210
+ sessionId,
211
+ deviceHash: computedDeviceHash,
212
+ machineName,
213
+ environmentClassification
214
+ });
215
+ if (!refreshedRunToken) {
216
+ return;
217
+ }
218
+ try {
219
+ await this.verifySignedRunToken(refreshedRunToken, request, requestedFeatures, runnerKey, sessionId, computedDeviceHash);
220
+ }
221
+ catch {
222
+ return;
223
+ }
224
+ session.runToken = refreshedRunToken;
225
+ }
226
+ catch {
227
+ // Best-effort heartbeat: server-side lease expiration is authoritative.
228
+ }
229
+ finally {
230
+ heartbeatInFlight = false;
231
+ }
213
232
  };
233
+ let currentHeartbeat = Promise.resolve();
234
+ const heartbeatTimer = setInterval(() => {
235
+ if (!heartbeatInFlight) {
236
+ currentHeartbeat = runHeartbeat();
237
+ }
238
+ return currentHeartbeat;
239
+ }, heartbeatIntervalSeconds * 1000);
240
+ if (typeof heartbeatTimer.unref === "function") {
241
+ heartbeatTimer.unref();
242
+ }
243
+ session.heartbeatTimer = heartbeatTimer;
244
+ __classPrivateFieldGet(this, _LoadStrikeLocalClient_heartbeatDrains, "f").set(session, () => currentHeartbeat);
245
+ return session;
214
246
  }
215
247
  finally {
216
248
  clearTimeout(timer);
@@ -394,18 +426,35 @@ export class LoadStrikeLocalClient {
394
426
  const timer = controller
395
427
  ? setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs)
396
428
  : null;
397
- const { response } = await this.postLicensingRequest("/api/v1/licenses/heartbeat", heartbeatPayload, signal ?? controller.signal);
429
+ const { response, json } = await this.postLicensingRequest("/api/v1/licenses/heartbeat", heartbeatPayload, signal ?? controller.signal);
398
430
  if (timer) {
399
431
  clearTimeout(timer);
400
432
  }
401
433
  if (!response.ok) {
402
434
  throw new Error(`Runner key validation denied. DenialCode=run_token_heartbeat_failed, Message=Run token heartbeat failed with status ${response.status}.`);
403
435
  }
436
+ if (pickValue(json ?? {}, "IsValid", "isValid") !== true) {
437
+ return undefined;
438
+ }
439
+ const refreshedRunToken = stringOrDefault(pickValue(json ?? {}, "RunToken", "runToken"), "").trim();
440
+ return refreshedRunToken || undefined;
404
441
  }
405
442
  async stopLicenseLeaseIfRequired(session, _request) {
406
443
  if (session.heartbeatTimer) {
407
444
  clearInterval(session.heartbeatTimer);
408
445
  }
446
+ const heartbeatDrain = __classPrivateFieldGet(this, _LoadStrikeLocalClient_heartbeatDrains, "f").get(session);
447
+ if (heartbeatDrain) {
448
+ try {
449
+ await heartbeatDrain();
450
+ }
451
+ catch {
452
+ // Heartbeats are best-effort and retain the last fully verified token.
453
+ }
454
+ finally {
455
+ __classPrivateFieldGet(this, _LoadStrikeLocalClient_heartbeatDrains, "f").delete(session);
456
+ }
457
+ }
409
458
  if (!session.runToken) {
410
459
  return;
411
460
  }
@@ -463,7 +512,7 @@ export class LoadStrikeLocalClient {
463
512
  return { response, json };
464
513
  }
465
514
  }
466
- _LoadStrikeLocalClient_licensingApiBaseUrl = new WeakMap(), _LoadStrikeLocalClient_signingKeyCache = new WeakMap();
515
+ _LoadStrikeLocalClient_licensingApiBaseUrl = new WeakMap(), _LoadStrikeLocalClient_signingKeyCache = new WeakMap(), _LoadStrikeLocalClient_heartbeatDrains = new WeakMap();
467
516
  function assertNoDisableLicenseEnforcementOption(value, source) {
468
517
  if (value == null || typeof value !== "object" || Array.isArray(value)) {
469
518
  return;
@@ -1827,6 +1876,10 @@ function isRuntimeReportingSink(value) {
1827
1876
  "SaveRealtimeMetrics",
1828
1877
  "saveRunResult",
1829
1878
  "SaveRunResult",
1879
+ "saveIterationBatch",
1880
+ "SaveIterationBatch",
1881
+ "completeIterationObservationStream",
1882
+ "CompleteIterationObservationStream",
1830
1883
  "stop",
1831
1884
  "Stop"
1832
1885
  ].some((name) => typeof record[name] === "function");
@@ -132,6 +132,26 @@ function asFloat(value) {
132
132
  const parsed = Number.parseFloat(asString(value));
133
133
  return Number.isFinite(parsed) ? parsed : 0;
134
134
  }
135
+ function combinedMeasurement(source) {
136
+ const all = reportValue(source, "allMeasurement", "AllMeasurement");
137
+ if (all && typeof all === "object" && !Array.isArray(all)) {
138
+ return all;
139
+ }
140
+ const ok = reportObject(source, "ok", "Ok");
141
+ const fail = reportObject(source, "fail", "Fail");
142
+ const okCount = asInt(reportValue(reportObject(ok, "request", "Request"), "count", "Count"));
143
+ const failCount = asInt(reportValue(reportObject(fail, "request", "Request"), "count", "Count"));
144
+ if (okCount === 0) {
145
+ return fail;
146
+ }
147
+ return failCount === 0 ? ok : undefined;
148
+ }
149
+ function formatCombinedLatency(source, ...keys) {
150
+ const measurement = combinedMeasurement(source);
151
+ return measurement
152
+ ? formatReportNumber(reportValue(reportObject(measurement, "latency", "Latency"), ...keys))
153
+ : "n/a";
154
+ }
135
155
  function asBool(value) {
136
156
  if (typeof value === "boolean") {
137
157
  return value;
@@ -747,8 +767,8 @@ function buildDotnetScenarioRows(nodeStats) {
747
767
  FAIL: reportFailCountValue(scenario),
748
768
  Duration: formatDotnetTimeSpan(reportDurationValue(scenario)),
749
769
  RPS: formatReportNumber(reportDurationSeconds(reportDurationValue(scenario)) <= 0 ? 0 : requestCount / reportDurationSeconds(reportDurationValue(scenario))),
750
- 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")))),
751
- 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")))),
770
+ LatencyP95Ms: formatCombinedLatency(scenario, "percent95", "Percent95"),
771
+ LatencyP99Ms: formatCombinedLatency(scenario, "percent99", "Percent99"),
752
772
  CurrentOperation: asString(reportValue(scenario, "currentOperation", "CurrentOperation"))
753
773
  };
754
774
  });
@@ -765,8 +785,8 @@ function buildDotnetStepRows(nodeStats) {
765
785
  FAIL: asInt(reportValue(reportObject(reportObject(step, "fail", "Fail"), "request", "Request"), "count", "Count")),
766
786
  OK_RPS: formatReportNumber(reportValue(reportObject(reportObject(step, "ok", "Ok"), "request", "Request"), "rps", "RPS")),
767
787
  FAIL_RPS: formatReportNumber(reportValue(reportObject(reportObject(step, "fail", "Fail"), "request", "Request"), "rps", "RPS")),
768
- 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")))),
769
- 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"))))
788
+ LatencyMeanMs: formatCombinedLatency(step, "meanMs", "MeanMs"),
789
+ P95LatencyMs: formatCombinedLatency(step, "percent95", "Percent95")
770
790
  });
771
791
  }
772
792
  }
@@ -776,6 +796,10 @@ function buildDotnetScenarioMeasurementRows(nodeStats) {
776
796
  const rows = [];
777
797
  for (const scenario of reportScenarios(nodeStats)) {
778
798
  const scenarioName = asString(reportValue(scenario, "scenarioName", "ScenarioName"));
799
+ const allMeasurement = reportValue(scenario, "allMeasurement", "AllMeasurement");
800
+ if (allMeasurement && typeof allMeasurement === "object" && !Array.isArray(allMeasurement)) {
801
+ pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "ALL", allMeasurement);
802
+ }
779
803
  pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "OK", reportObject(scenario, "ok", "Ok"));
780
804
  pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "FAIL", reportObject(scenario, "fail", "Fail"));
781
805
  }
@@ -787,6 +811,10 @@ function buildDotnetStepMeasurementRows(nodeStats) {
787
811
  const scenarioName = asString(reportValue(scenario, "scenarioName", "ScenarioName"));
788
812
  for (const step of reportSteps(scenario)) {
789
813
  const stepName = asString(reportValue(step, "stepName", "StepName"));
814
+ const allMeasurement = reportValue(step, "allMeasurement", "AllMeasurement");
815
+ if (allMeasurement && typeof allMeasurement === "object" && !Array.isArray(allMeasurement)) {
816
+ pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "ALL", allMeasurement);
817
+ }
790
818
  pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "OK", reportObject(step, "ok", "Ok"));
791
819
  pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "FAIL", reportObject(step, "fail", "Fail"));
792
820
  }
@@ -867,6 +895,9 @@ function buildDotnetStatusCodeClassChart(scenarios) {
867
895
  }
868
896
  function buildDotnetChartData(nodeStats) {
869
897
  const scenarios = reportScenarios(nodeStats);
898
+ const combinedScenarios = scenarios
899
+ .map((scenario) => ({ scenario, measurement: combinedMeasurement(scenario) }))
900
+ .filter((item) => item.measurement !== undefined);
870
901
  return {
871
902
  overallOutcome: [
872
903
  { label: "OK", value: reportTotalOkCount(nodeStats, scenarios), color: "#18a957" },
@@ -877,9 +908,9 @@ function buildDotnetChartData(nodeStats) {
877
908
  value: reportRequestCountValue(scenario),
878
909
  color: "#3b82f6"
879
910
  })),
880
- scenarioP95Latency: scenarios.map((scenario) => ({
911
+ scenarioP95Latency: combinedScenarios.map(({ scenario, measurement }) => ({
881
912
  label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
882
- 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"))),
913
+ value: asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95")),
883
914
  color: "#8b5cf6"
884
915
  })),
885
916
  scenarioRps: scenarios.map((scenario) => ({
@@ -903,12 +934,12 @@ function buildDotnetChartData(nodeStats) {
903
934
  })),
904
935
  statusCodeClasses: buildDotnetStatusCodeClassChart(scenarios),
905
936
  scenarioLatencyTrend: {
906
- labels: scenarios.map((scenario) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
937
+ labels: combinedScenarios.map(({ scenario }) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
907
938
  series: [
908
- { 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")))) },
909
- { 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")))) },
910
- { 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")))) },
911
- { 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")))) }
939
+ { name: "P50", color: "#38bdf8", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent50", "Percent50"))) },
940
+ { name: "P75", color: "#22c55e", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent75", "Percent75"))) },
941
+ { name: "P95", color: "#f59e0b", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95"))) },
942
+ { name: "P99", color: "#f43f5e", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent99", "Percent99"))) }
912
943
  ]
913
944
  }
914
945
  };
@@ -1053,6 +1084,82 @@ function buildDotnetThresholdHtml(nodeStats) {
1053
1084
  function buildDotnetMetricHtml(nodeStats) {
1054
1085
  return buildDotnetTableHtml(buildDotnetMetricRows(nodeStats));
1055
1086
  }
1087
+ function buildDotnetGeneratorDeliveryHtml(nodeStats) {
1088
+ const warnings = reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings");
1089
+ const stats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
1090
+ const topLevelSegments = reportArray(nodeStats, "schedulerSegments", "SchedulerSegments");
1091
+ const segments = topLevelSegments.length > 0
1092
+ ? topLevelSegments
1093
+ : reportArray(stats, "segments", "Segments");
1094
+ const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
1095
+ const reportingCompleteValue = reportValue(nodeStats, "reportingComplete", "ReportingComplete");
1096
+ const reportingComplete = reportingCompleteValue == null
1097
+ ? "N/A"
1098
+ : asBool(reportingCompleteValue) ? "Yes" : "No";
1099
+ const parts = [];
1100
+ appendReportLine(parts, "<div class=\"card\">");
1101
+ appendReportLine(parts, "<h2>Generator Delivery</h2>");
1102
+ 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>");
1103
+ appendReportLine(parts, "<div class=\"card-grid\">");
1104
+ for (const [label, value] of [
1105
+ ["Configured Max In Flight", asInt(reportValue(stats, "configuredMaxInFlight", "ConfiguredMaxInFlight"))],
1106
+ ["Observed Max In Flight", asInt(reportValue(stats, "maxInFlightObserved", "MaxInFlightObserved"))],
1107
+ ["Current In Flight", asInt(reportValue(stats, "currentInFlight", "CurrentInFlight"))],
1108
+ ["Observation Captured", asString(reportValue(observationStats, "capturedCount64", "CapturedCount64")) || "0"],
1109
+ ["Observation Delivered", asString(reportValue(observationStats, "deliveredCount64", "DeliveredCount64")) || "0"],
1110
+ ["Observation Buffer Drops", asString(reportValue(observationStats, "droppedBufferCount64", "DroppedBufferCount64")) || "0"],
1111
+ ["Observation Sink Drops", asString(reportValue(observationStats, "droppedSinkCount64", "DroppedSinkCount64")) || "0"],
1112
+ ["Reporting Complete", reportingComplete],
1113
+ ["Warning Groups", warnings.length]
1114
+ ]) {
1115
+ appendReportLine(parts, `<div class="stat-card"><div class="stat-label">${escapeHtml(label)}</div><div class="stat-value">${escapeHtml(value)}</div></div>`);
1116
+ }
1117
+ appendReportLine(parts, "</div></div>");
1118
+ if (warnings.length) {
1119
+ appendReportLine(parts, "<div class=\"card\"><h2>Generator Warnings</h2>");
1120
+ parts.push(buildDotnetTableHtml(warnings, false));
1121
+ appendReportLine(parts, "</div>");
1122
+ }
1123
+ if (segments.length) {
1124
+ const rows = segments.map((segment) => ({
1125
+ Scenario: asString(reportValue(segment, "scenarioName", "ScenarioName")),
1126
+ Simulation: asString(reportValue(segment, "kind", "Kind")),
1127
+ Shard: `${asInt(reportValue(segment, "shardIndex", "ShardIndex"))}/${Math.max(asInt(reportValue(segment, "shardCount", "ShardCount")), 1)}`,
1128
+ Planned: asString(reportValue(segment, "plannedIterations64", "PlannedIterations64")),
1129
+ Due: asString(reportValue(segment, "dueIterations64", "DueIterations64")),
1130
+ Started: asString(reportValue(segment, "startedIterations64", "StartedIterations64")),
1131
+ Completed: asString(reportValue(segment, "completedIterations64", "CompletedIterations64")),
1132
+ Dropped: asString(reportValue(segment, "droppedIterations64", "DroppedIterations64")),
1133
+ Unreached: asString(reportValue(segment, "unreachedIterations64", "UnreachedIterations64")),
1134
+ "Unavailable Workers": asString(reportValue(segment, "unavailableWorkerSlots64", "UnavailableWorkerSlots64")),
1135
+ "Delivery %": formatReportNumber(reportValue(segment, "deliveryPercent", "DeliveryPercent")),
1136
+ "Accounting Complete": Boolean(reportValue(segment, "accountingComplete", "AccountingComplete"))
1137
+ }));
1138
+ appendReportLine(parts, "<div class=\"card\"><h2>Scheduler Segments</h2>");
1139
+ parts.push(buildDotnetTableHtml(rows, false));
1140
+ appendReportLine(parts, "</div>");
1141
+ }
1142
+ return parts.join("");
1143
+ }
1144
+ function hasDotnetGeneratorDeliveryData(nodeStats) {
1145
+ const schedulerStats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
1146
+ const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
1147
+ const hasNonZeroDecimal = (value) => {
1148
+ const text = asString(value);
1149
+ return text.trim().length > 0 && text !== "0";
1150
+ };
1151
+ return reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings").length > 0
1152
+ || reportArray(nodeStats, "schedulerSegments", "SchedulerSegments").length > 0
1153
+ || reportArray(schedulerStats, "segments", "Segments").length > 0
1154
+ || asInt(reportValue(schedulerStats, "configuredMaxInFlight", "ConfiguredMaxInFlight")) > 0
1155
+ || asInt(reportValue(schedulerStats, "maxInFlightObserved", "MaxInFlightObserved")) > 0
1156
+ || asInt(reportValue(schedulerStats, "currentInFlight", "CurrentInFlight")) > 0
1157
+ || reportValue(nodeStats, "reportingComplete", "ReportingComplete") === false
1158
+ || hasNonZeroDecimal(reportValue(observationStats, "capturedCount64", "CapturedCount64"))
1159
+ || hasNonZeroDecimal(reportValue(observationStats, "deliveredCount64", "DeliveredCount64"))
1160
+ || hasNonZeroDecimal(reportValue(observationStats, "droppedBufferCount64", "DroppedBufferCount64"))
1161
+ || hasNonZeroDecimal(reportValue(observationStats, "droppedSinkCount64", "DroppedSinkCount64"));
1162
+ }
1056
1163
  function buildDotnetGroupedCorrelationSummaryHtml(rows, groupedChartKey) {
1057
1164
  const parts = [];
1058
1165
  const payloads = buildGroupedCorrelationChartPayloads(rows);
@@ -1129,6 +1236,9 @@ function buildDotnetHtmlTabs(nodeStats) {
1129
1236
  if (metricRows.length) {
1130
1237
  tabs.push(["metrics", "Metrics", buildDotnetTableHtml(metricRows)]);
1131
1238
  }
1239
+ if (hasDotnetGeneratorDeliveryData(nodeStats)) {
1240
+ tabs.push(["generator-delivery", "Generator Delivery", buildDotnetGeneratorDeliveryHtml(nodeStats)]);
1241
+ }
1132
1242
  for (const plugin of reportArray(nodeStats, "pluginsData", "PluginsData")) {
1133
1243
  const pluginName = asString(reportValue(plugin, "pluginName", "PluginName"));
1134
1244
  const hints = buildDotnetPluginHints(plugin);
@@ -1372,7 +1482,7 @@ const reportLogo=document.querySelector('[data-report-logo]');
1372
1482
  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');}}
1373
1483
  function show(id){btns.forEach(b=>b.classList.toggle('active',b.dataset.tab===id));tabSections.forEach(t=>t.classList.toggle('active',t.id===id));}
1374
1484
  function formatMetric(v){if(!Number.isFinite(v))return '0';return Math.abs(v)>=100?v.toFixed(0):v.toFixed(2);}
1375
- 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};}
1485
+ 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};}
1376
1486
  function drawNoData(ctx,w,h,msg){ctx.fillStyle='#9fb0c3';ctx.font='13px Segoe UI';ctx.textAlign='center';ctx.fillText(msg,w/2,h/2);}
1377
1487
  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);}}
1378
1488
  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;}}