@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/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 +122 -12
- 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 +122 -12
- 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/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
|
|
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
|
-
|
|
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");
|
package/dist/esm/reporting.js
CHANGED
|
@@ -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:
|
|
751
|
-
LatencyP99Ms:
|
|
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:
|
|
769
|
-
P95LatencyMs:
|
|
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:
|
|
911
|
+
scenarioP95Latency: combinedScenarios.map(({ scenario, measurement }) => ({
|
|
881
912
|
label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
|
|
882
|
-
value:
|
|
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:
|
|
937
|
+
labels: combinedScenarios.map(({ scenario }) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
|
|
907
938
|
series: [
|
|
908
|
-
{ name: "P50", color: "#38bdf8", values:
|
|
909
|
-
{ name: "P75", color: "#22c55e", values:
|
|
910
|
-
{ name: "P95", color: "#f59e0b", values:
|
|
911
|
-
{ name: "P99", color: "#f43f5e", values:
|
|
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?'☀':'☾';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(
|
|
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;}}
|