@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/README.md +24 -0
- package/dist/cjs/cluster.js +2417 -7
- package/dist/cjs/index.js +13 -2
- package/dist/cjs/iteration-observations.js +711 -0
- package/dist/cjs/load-engine-v2.js +966 -0
- package/dist/cjs/local.js +73 -20
- package/dist/cjs/reporting.js +123 -13
- package/dist/cjs/runtime.js +2368 -129
- package/dist/cjs/sinks.js +471 -17
- package/dist/cjs/transports.js +84 -146
- package/dist/esm/cluster.js +2386 -7
- package/dist/esm/index.js +1 -0
- package/dist/esm/iteration-observations.js +698 -0
- package/dist/esm/load-engine-v2.js +942 -0
- package/dist/esm/local.js +73 -20
- package/dist/esm/reporting.js +123 -13
- package/dist/esm/runtime.js +2369 -130
- package/dist/esm/sinks.js +471 -17
- package/dist/esm/transports.js +84 -146
- package/dist/types/cluster.d.ts +379 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/iteration-observations.d.ts +225 -0
- package/dist/types/load-engine-v2.d.ts +147 -0
- package/dist/types/runtime.d.ts +214 -8
- package/dist/types/sinks.d.ts +67 -0
- package/dist/types/transports.d.ts +2 -8
- package/package.json +3 -4
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
|
|
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
|
-
|
|
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");
|
package/dist/cjs/reporting.js
CHANGED
|
@@ -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:
|
|
758
|
-
LatencyP99Ms:
|
|
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:
|
|
776
|
-
P95LatencyMs:
|
|
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:
|
|
918
|
+
scenarioP95Latency: combinedScenarios.map(({ scenario, measurement }) => ({
|
|
888
919
|
label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
|
|
889
|
-
value:
|
|
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:
|
|
944
|
+
labels: combinedScenarios.map(({ scenario }) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
|
|
914
945
|
series: [
|
|
915
|
-
{ name: "P50", color: "#38bdf8", values:
|
|
916
|
-
{ name: "P75", color: "#22c55e", values:
|
|
917
|
-
{ name: "P95", color: "#f59e0b", values:
|
|
918
|
-
{ name: "P99", color: "#f43f5e", values:
|
|
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?'☀':'☾';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(
|
|
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>
|