@loadstrike/loadstrike-sdk 1.0.30201 → 1.0.31001
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 +28 -0
- package/dist/cjs/cluster.js +2417 -7
- package/dist/cjs/index.js +13 -2
- package/dist/cjs/iteration-observation-diagnostics.js +513 -0
- package/dist/cjs/iteration-observations.js +884 -0
- package/dist/cjs/load-engine-v2.js +966 -0
- package/dist/cjs/local.js +128 -30
- package/dist/cjs/reporting.js +148 -19
- package/dist/cjs/runtime.js +2514 -196
- package/dist/cjs/sink-retry-policy.js +52 -0
- package/dist/cjs/sinks.js +580 -23
- package/dist/cjs/transports.js +154 -163
- package/dist/esm/cluster.js +2386 -7
- package/dist/esm/index.js +1 -0
- package/dist/esm/iteration-observation-diagnostics.js +508 -0
- package/dist/esm/iteration-observations.js +871 -0
- package/dist/esm/load-engine-v2.js +942 -0
- package/dist/esm/local.js +128 -30
- package/dist/esm/reporting.js +148 -19
- package/dist/esm/runtime.js +2515 -197
- package/dist/esm/sink-retry-policy.js +44 -0
- package/dist/esm/sinks.js +580 -23
- package/dist/esm/transports.js +154 -163
- package/dist/types/cluster.d.ts +379 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/iteration-observation-diagnostics.d.ts +21 -0
- package/dist/types/iteration-observations.d.ts +230 -0
- package/dist/types/load-engine-v2.d.ts +147 -0
- package/dist/types/runtime.d.ts +216 -8
- package/dist/types/sink-retry-policy.d.ts +9 -0
- package/dist/types/sinks.d.ts +73 -0
- package/dist/types/transports.d.ts +6 -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
|
-
heartbeatTimer
|
|
237
|
+
environmentClassification
|
|
252
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
|
+
}
|
|
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;
|
|
@@ -1369,9 +1418,20 @@ function readTrackingId(payload, selector) {
|
|
|
1369
1418
|
return null;
|
|
1370
1419
|
}
|
|
1371
1420
|
let current = body;
|
|
1372
|
-
|
|
1421
|
+
let segments;
|
|
1422
|
+
try {
|
|
1423
|
+
segments = safeJsonPathSegments(path);
|
|
1424
|
+
}
|
|
1425
|
+
catch {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
for (const segment of segments) {
|
|
1373
1429
|
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1374
|
-
|
|
1430
|
+
const record = current;
|
|
1431
|
+
if (!Object.prototype.hasOwnProperty.call(record, segment)) {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
current = record[segment];
|
|
1375
1435
|
}
|
|
1376
1436
|
else {
|
|
1377
1437
|
return null;
|
|
@@ -1404,23 +1464,57 @@ function readOptionalTrackingSelectorValue(value) {
|
|
|
1404
1464
|
return undefined;
|
|
1405
1465
|
}
|
|
1406
1466
|
function setJsonPathValue(body, path, value) {
|
|
1407
|
-
const target =
|
|
1408
|
-
|
|
1409
|
-
: {};
|
|
1410
|
-
const segments = path.split(".").filter(Boolean);
|
|
1467
|
+
const target = cloneJsonRecord(body);
|
|
1468
|
+
const segments = safeJsonPathSegments(path);
|
|
1411
1469
|
if (!segments.length) {
|
|
1412
1470
|
return target;
|
|
1413
1471
|
}
|
|
1414
1472
|
let current = target;
|
|
1415
1473
|
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
1416
1474
|
const segment = segments[i];
|
|
1417
|
-
const next = current
|
|
1475
|
+
const next = readOwnJsonProperty(current, segment);
|
|
1476
|
+
let child;
|
|
1418
1477
|
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
1419
|
-
|
|
1478
|
+
child = {};
|
|
1420
1479
|
}
|
|
1421
|
-
|
|
1480
|
+
else {
|
|
1481
|
+
child = cloneJsonRecord(next);
|
|
1482
|
+
}
|
|
1483
|
+
defineJsonProperty(current, segment, child);
|
|
1484
|
+
current = child;
|
|
1485
|
+
}
|
|
1486
|
+
defineJsonProperty(current, segments[segments.length - 1], value);
|
|
1487
|
+
return target;
|
|
1488
|
+
}
|
|
1489
|
+
const FORBIDDEN_JSON_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
1490
|
+
function safeJsonPathSegments(path) {
|
|
1491
|
+
const segments = path.split(".").filter(Boolean);
|
|
1492
|
+
const forbidden = segments.find((segment) => FORBIDDEN_JSON_PATH_SEGMENTS.has(segment));
|
|
1493
|
+
if (forbidden) {
|
|
1494
|
+
throw new Error(`Tracking selector contains forbidden JSON path segment '${forbidden}'.`);
|
|
1495
|
+
}
|
|
1496
|
+
return segments;
|
|
1497
|
+
}
|
|
1498
|
+
function defineJsonProperty(target, key, value) {
|
|
1499
|
+
Object.defineProperty(target, key, {
|
|
1500
|
+
configurable: true,
|
|
1501
|
+
enumerable: true,
|
|
1502
|
+
value,
|
|
1503
|
+
writable: true
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
function readOwnJsonProperty(target, key) {
|
|
1507
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
1508
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
1509
|
+
}
|
|
1510
|
+
function cloneJsonRecord(value) {
|
|
1511
|
+
const target = {};
|
|
1512
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1513
|
+
return target;
|
|
1514
|
+
}
|
|
1515
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1516
|
+
defineJsonProperty(target, key, entry);
|
|
1422
1517
|
}
|
|
1423
|
-
current[segments[segments.length - 1]] = value;
|
|
1424
1518
|
return target;
|
|
1425
1519
|
}
|
|
1426
1520
|
function mapCorrelationStore(tracking, runNamespace) {
|
|
@@ -1867,6 +1961,10 @@ function isRuntimeReportingSink(value) {
|
|
|
1867
1961
|
"SaveRealtimeMetrics",
|
|
1868
1962
|
"saveRunResult",
|
|
1869
1963
|
"SaveRunResult",
|
|
1964
|
+
"saveIterationBatch",
|
|
1965
|
+
"SaveIterationBatch",
|
|
1966
|
+
"completeIterationObservationStream",
|
|
1967
|
+
"CompleteIterationObservationStream",
|
|
1870
1968
|
"stop",
|
|
1871
1969
|
"Stop"
|
|
1872
1970
|
].some((name) => typeof record[name] === "function");
|
package/dist/cjs/reporting.js
CHANGED
|
@@ -45,8 +45,12 @@ function reportValue(source, ...keys) {
|
|
|
45
45
|
}
|
|
46
46
|
const record = source;
|
|
47
47
|
for (const key of keys) {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
49
|
+
if (descriptor
|
|
50
|
+
&& "value" in descriptor
|
|
51
|
+
&& descriptor.value !== undefined
|
|
52
|
+
&& descriptor.value !== null) {
|
|
53
|
+
return descriptor.value;
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
return undefined;
|
|
@@ -139,6 +143,26 @@ function asFloat(value) {
|
|
|
139
143
|
const parsed = Number.parseFloat(asString(value));
|
|
140
144
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
141
145
|
}
|
|
146
|
+
function combinedMeasurement(source) {
|
|
147
|
+
const all = reportValue(source, "allMeasurement", "AllMeasurement");
|
|
148
|
+
if (all && typeof all === "object" && !Array.isArray(all)) {
|
|
149
|
+
return all;
|
|
150
|
+
}
|
|
151
|
+
const ok = reportObject(source, "ok", "Ok");
|
|
152
|
+
const fail = reportObject(source, "fail", "Fail");
|
|
153
|
+
const okCount = asInt(reportValue(reportObject(ok, "request", "Request"), "count", "Count"));
|
|
154
|
+
const failCount = asInt(reportValue(reportObject(fail, "request", "Request"), "count", "Count"));
|
|
155
|
+
if (okCount === 0) {
|
|
156
|
+
return fail;
|
|
157
|
+
}
|
|
158
|
+
return failCount === 0 ? ok : undefined;
|
|
159
|
+
}
|
|
160
|
+
function formatCombinedLatency(source, ...keys) {
|
|
161
|
+
const measurement = combinedMeasurement(source);
|
|
162
|
+
return measurement
|
|
163
|
+
? formatReportNumber(reportValue(reportObject(measurement, "latency", "Latency"), ...keys))
|
|
164
|
+
: "n/a";
|
|
165
|
+
}
|
|
142
166
|
function asBool(value) {
|
|
143
167
|
if (typeof value === "boolean") {
|
|
144
168
|
return value;
|
|
@@ -309,7 +333,22 @@ function formatDotnetDateTime(value) {
|
|
|
309
333
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${fraction}Z`;
|
|
310
334
|
}
|
|
311
335
|
function escapeJsonForHtmlScript(value) {
|
|
312
|
-
return value.
|
|
336
|
+
return value.replace(/[<>&\u2028\u2029]/g, (character) => {
|
|
337
|
+
switch (character) {
|
|
338
|
+
case "<":
|
|
339
|
+
return "\\u003c";
|
|
340
|
+
case ">":
|
|
341
|
+
return "\\u003e";
|
|
342
|
+
case "&":
|
|
343
|
+
return "\\u0026";
|
|
344
|
+
case "\u2028":
|
|
345
|
+
return "\\u2028";
|
|
346
|
+
case "\u2029":
|
|
347
|
+
return "\\u2029";
|
|
348
|
+
default:
|
|
349
|
+
return character;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
313
352
|
}
|
|
314
353
|
function buildReportLogoDataUri(resourceName) {
|
|
315
354
|
const cached = REPORT_LOGO_CACHE.get(resourceName);
|
|
@@ -754,8 +793,8 @@ function buildDotnetScenarioRows(nodeStats) {
|
|
|
754
793
|
FAIL: reportFailCountValue(scenario),
|
|
755
794
|
Duration: formatDotnetTimeSpan(reportDurationValue(scenario)),
|
|
756
795
|
RPS: formatReportNumber(reportDurationSeconds(reportDurationValue(scenario)) <= 0 ? 0 : requestCount / reportDurationSeconds(reportDurationValue(scenario))),
|
|
757
|
-
LatencyP95Ms:
|
|
758
|
-
LatencyP99Ms:
|
|
796
|
+
LatencyP95Ms: formatCombinedLatency(scenario, "percent95", "Percent95"),
|
|
797
|
+
LatencyP99Ms: formatCombinedLatency(scenario, "percent99", "Percent99"),
|
|
759
798
|
CurrentOperation: asString(reportValue(scenario, "currentOperation", "CurrentOperation"))
|
|
760
799
|
};
|
|
761
800
|
});
|
|
@@ -772,8 +811,8 @@ function buildDotnetStepRows(nodeStats) {
|
|
|
772
811
|
FAIL: asInt(reportValue(reportObject(reportObject(step, "fail", "Fail"), "request", "Request"), "count", "Count")),
|
|
773
812
|
OK_RPS: formatReportNumber(reportValue(reportObject(reportObject(step, "ok", "Ok"), "request", "Request"), "rps", "RPS")),
|
|
774
813
|
FAIL_RPS: formatReportNumber(reportValue(reportObject(reportObject(step, "fail", "Fail"), "request", "Request"), "rps", "RPS")),
|
|
775
|
-
LatencyMeanMs:
|
|
776
|
-
P95LatencyMs:
|
|
814
|
+
LatencyMeanMs: formatCombinedLatency(step, "meanMs", "MeanMs"),
|
|
815
|
+
P95LatencyMs: formatCombinedLatency(step, "percent95", "Percent95")
|
|
777
816
|
});
|
|
778
817
|
}
|
|
779
818
|
}
|
|
@@ -783,6 +822,10 @@ function buildDotnetScenarioMeasurementRows(nodeStats) {
|
|
|
783
822
|
const rows = [];
|
|
784
823
|
for (const scenario of reportScenarios(nodeStats)) {
|
|
785
824
|
const scenarioName = asString(reportValue(scenario, "scenarioName", "ScenarioName"));
|
|
825
|
+
const allMeasurement = reportValue(scenario, "allMeasurement", "AllMeasurement");
|
|
826
|
+
if (allMeasurement && typeof allMeasurement === "object" && !Array.isArray(allMeasurement)) {
|
|
827
|
+
pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "ALL", allMeasurement);
|
|
828
|
+
}
|
|
786
829
|
pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "OK", reportObject(scenario, "ok", "Ok"));
|
|
787
830
|
pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "FAIL", reportObject(scenario, "fail", "Fail"));
|
|
788
831
|
}
|
|
@@ -794,6 +837,10 @@ function buildDotnetStepMeasurementRows(nodeStats) {
|
|
|
794
837
|
const scenarioName = asString(reportValue(scenario, "scenarioName", "ScenarioName"));
|
|
795
838
|
for (const step of reportSteps(scenario)) {
|
|
796
839
|
const stepName = asString(reportValue(step, "stepName", "StepName"));
|
|
840
|
+
const allMeasurement = reportValue(step, "allMeasurement", "AllMeasurement");
|
|
841
|
+
if (allMeasurement && typeof allMeasurement === "object" && !Array.isArray(allMeasurement)) {
|
|
842
|
+
pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "ALL", allMeasurement);
|
|
843
|
+
}
|
|
797
844
|
pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "OK", reportObject(step, "ok", "Ok"));
|
|
798
845
|
pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "FAIL", reportObject(step, "fail", "Fail"));
|
|
799
846
|
}
|
|
@@ -874,6 +921,9 @@ function buildDotnetStatusCodeClassChart(scenarios) {
|
|
|
874
921
|
}
|
|
875
922
|
function buildDotnetChartData(nodeStats) {
|
|
876
923
|
const scenarios = reportScenarios(nodeStats);
|
|
924
|
+
const combinedScenarios = scenarios
|
|
925
|
+
.map((scenario) => ({ scenario, measurement: combinedMeasurement(scenario) }))
|
|
926
|
+
.filter((item) => item.measurement !== undefined);
|
|
877
927
|
return {
|
|
878
928
|
overallOutcome: [
|
|
879
929
|
{ label: "OK", value: reportTotalOkCount(nodeStats, scenarios), color: "#18a957" },
|
|
@@ -884,9 +934,9 @@ function buildDotnetChartData(nodeStats) {
|
|
|
884
934
|
value: reportRequestCountValue(scenario),
|
|
885
935
|
color: "#3b82f6"
|
|
886
936
|
})),
|
|
887
|
-
scenarioP95Latency:
|
|
937
|
+
scenarioP95Latency: combinedScenarios.map(({ scenario, measurement }) => ({
|
|
888
938
|
label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
|
|
889
|
-
value:
|
|
939
|
+
value: asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95")),
|
|
890
940
|
color: "#8b5cf6"
|
|
891
941
|
})),
|
|
892
942
|
scenarioRps: scenarios.map((scenario) => ({
|
|
@@ -910,12 +960,12 @@ function buildDotnetChartData(nodeStats) {
|
|
|
910
960
|
})),
|
|
911
961
|
statusCodeClasses: buildDotnetStatusCodeClassChart(scenarios),
|
|
912
962
|
scenarioLatencyTrend: {
|
|
913
|
-
labels:
|
|
963
|
+
labels: combinedScenarios.map(({ scenario }) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
|
|
914
964
|
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:
|
|
965
|
+
{ name: "P50", color: "#38bdf8", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent50", "Percent50"))) },
|
|
966
|
+
{ name: "P75", color: "#22c55e", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent75", "Percent75"))) },
|
|
967
|
+
{ name: "P95", color: "#f59e0b", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95"))) },
|
|
968
|
+
{ name: "P99", color: "#f43f5e", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent99", "Percent99"))) }
|
|
919
969
|
]
|
|
920
970
|
}
|
|
921
971
|
};
|
|
@@ -1060,6 +1110,82 @@ function buildDotnetThresholdHtml(nodeStats) {
|
|
|
1060
1110
|
function buildDotnetMetricHtml(nodeStats) {
|
|
1061
1111
|
return buildDotnetTableHtml(buildDotnetMetricRows(nodeStats));
|
|
1062
1112
|
}
|
|
1113
|
+
function buildDotnetGeneratorDeliveryHtml(nodeStats) {
|
|
1114
|
+
const warnings = reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings");
|
|
1115
|
+
const stats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
|
|
1116
|
+
const topLevelSegments = reportArray(nodeStats, "schedulerSegments", "SchedulerSegments");
|
|
1117
|
+
const segments = topLevelSegments.length > 0
|
|
1118
|
+
? topLevelSegments
|
|
1119
|
+
: reportArray(stats, "segments", "Segments");
|
|
1120
|
+
const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
|
|
1121
|
+
const reportingCompleteValue = reportValue(nodeStats, "reportingComplete", "ReportingComplete");
|
|
1122
|
+
const reportingComplete = reportingCompleteValue == null
|
|
1123
|
+
? "N/A"
|
|
1124
|
+
: asBool(reportingCompleteValue) ? "Yes" : "No";
|
|
1125
|
+
const parts = [];
|
|
1126
|
+
appendReportLine(parts, "<div class=\"card\">");
|
|
1127
|
+
appendReportLine(parts, "<h2>Generator Delivery</h2>");
|
|
1128
|
+
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>");
|
|
1129
|
+
appendReportLine(parts, "<div class=\"card-grid\">");
|
|
1130
|
+
for (const [label, value] of [
|
|
1131
|
+
["Configured Max In Flight", asInt(reportValue(stats, "configuredMaxInFlight", "ConfiguredMaxInFlight"))],
|
|
1132
|
+
["Observed Max In Flight", asInt(reportValue(stats, "maxInFlightObserved", "MaxInFlightObserved"))],
|
|
1133
|
+
["Current In Flight", asInt(reportValue(stats, "currentInFlight", "CurrentInFlight"))],
|
|
1134
|
+
["Observation Captured", asString(reportValue(observationStats, "capturedCount64", "CapturedCount64")) || "0"],
|
|
1135
|
+
["Observation Delivered", asString(reportValue(observationStats, "deliveredCount64", "DeliveredCount64")) || "0"],
|
|
1136
|
+
["Observation Buffer Drops", asString(reportValue(observationStats, "droppedBufferCount64", "DroppedBufferCount64")) || "0"],
|
|
1137
|
+
["Observation Sink Drops", asString(reportValue(observationStats, "droppedSinkCount64", "DroppedSinkCount64")) || "0"],
|
|
1138
|
+
["Reporting Complete", reportingComplete],
|
|
1139
|
+
["Warning Groups", warnings.length]
|
|
1140
|
+
]) {
|
|
1141
|
+
appendReportLine(parts, `<div class="stat-card"><div class="stat-label">${escapeHtml(label)}</div><div class="stat-value">${escapeHtml(value)}</div></div>`);
|
|
1142
|
+
}
|
|
1143
|
+
appendReportLine(parts, "</div></div>");
|
|
1144
|
+
if (warnings.length) {
|
|
1145
|
+
appendReportLine(parts, "<div class=\"card\"><h2>Generator Warnings</h2>");
|
|
1146
|
+
parts.push(buildDotnetTableHtml(warnings, false));
|
|
1147
|
+
appendReportLine(parts, "</div>");
|
|
1148
|
+
}
|
|
1149
|
+
if (segments.length) {
|
|
1150
|
+
const rows = segments.map((segment) => ({
|
|
1151
|
+
Scenario: asString(reportValue(segment, "scenarioName", "ScenarioName")),
|
|
1152
|
+
Simulation: asString(reportValue(segment, "kind", "Kind")),
|
|
1153
|
+
Shard: `${asInt(reportValue(segment, "shardIndex", "ShardIndex"))}/${Math.max(asInt(reportValue(segment, "shardCount", "ShardCount")), 1)}`,
|
|
1154
|
+
Planned: asString(reportValue(segment, "plannedIterations64", "PlannedIterations64")),
|
|
1155
|
+
Due: asString(reportValue(segment, "dueIterations64", "DueIterations64")),
|
|
1156
|
+
Started: asString(reportValue(segment, "startedIterations64", "StartedIterations64")),
|
|
1157
|
+
Completed: asString(reportValue(segment, "completedIterations64", "CompletedIterations64")),
|
|
1158
|
+
Dropped: asString(reportValue(segment, "droppedIterations64", "DroppedIterations64")),
|
|
1159
|
+
Unreached: asString(reportValue(segment, "unreachedIterations64", "UnreachedIterations64")),
|
|
1160
|
+
"Unavailable Workers": asString(reportValue(segment, "unavailableWorkerSlots64", "UnavailableWorkerSlots64")),
|
|
1161
|
+
"Delivery %": formatReportNumber(reportValue(segment, "deliveryPercent", "DeliveryPercent")),
|
|
1162
|
+
"Accounting Complete": Boolean(reportValue(segment, "accountingComplete", "AccountingComplete"))
|
|
1163
|
+
}));
|
|
1164
|
+
appendReportLine(parts, "<div class=\"card\"><h2>Scheduler Segments</h2>");
|
|
1165
|
+
parts.push(buildDotnetTableHtml(rows, false));
|
|
1166
|
+
appendReportLine(parts, "</div>");
|
|
1167
|
+
}
|
|
1168
|
+
return parts.join("");
|
|
1169
|
+
}
|
|
1170
|
+
function hasDotnetGeneratorDeliveryData(nodeStats) {
|
|
1171
|
+
const schedulerStats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
|
|
1172
|
+
const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
|
|
1173
|
+
const hasNonZeroDecimal = (value) => {
|
|
1174
|
+
const text = asString(value);
|
|
1175
|
+
return text.trim().length > 0 && text !== "0";
|
|
1176
|
+
};
|
|
1177
|
+
return reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings").length > 0
|
|
1178
|
+
|| reportArray(nodeStats, "schedulerSegments", "SchedulerSegments").length > 0
|
|
1179
|
+
|| reportArray(schedulerStats, "segments", "Segments").length > 0
|
|
1180
|
+
|| asInt(reportValue(schedulerStats, "configuredMaxInFlight", "ConfiguredMaxInFlight")) > 0
|
|
1181
|
+
|| asInt(reportValue(schedulerStats, "maxInFlightObserved", "MaxInFlightObserved")) > 0
|
|
1182
|
+
|| asInt(reportValue(schedulerStats, "currentInFlight", "CurrentInFlight")) > 0
|
|
1183
|
+
|| reportValue(nodeStats, "reportingComplete", "ReportingComplete") === false
|
|
1184
|
+
|| hasNonZeroDecimal(reportValue(observationStats, "capturedCount64", "CapturedCount64"))
|
|
1185
|
+
|| hasNonZeroDecimal(reportValue(observationStats, "deliveredCount64", "DeliveredCount64"))
|
|
1186
|
+
|| hasNonZeroDecimal(reportValue(observationStats, "droppedBufferCount64", "DroppedBufferCount64"))
|
|
1187
|
+
|| hasNonZeroDecimal(reportValue(observationStats, "droppedSinkCount64", "DroppedSinkCount64"));
|
|
1188
|
+
}
|
|
1063
1189
|
function buildDotnetGroupedCorrelationSummaryHtml(rows, groupedChartKey) {
|
|
1064
1190
|
const parts = [];
|
|
1065
1191
|
const payloads = buildGroupedCorrelationChartPayloads(rows);
|
|
@@ -1136,6 +1262,9 @@ function buildDotnetHtmlTabs(nodeStats) {
|
|
|
1136
1262
|
if (metricRows.length) {
|
|
1137
1263
|
tabs.push(["metrics", "Metrics", buildDotnetTableHtml(metricRows)]);
|
|
1138
1264
|
}
|
|
1265
|
+
if (hasDotnetGeneratorDeliveryData(nodeStats)) {
|
|
1266
|
+
tabs.push(["generator-delivery", "Generator Delivery", buildDotnetGeneratorDeliveryHtml(nodeStats)]);
|
|
1267
|
+
}
|
|
1139
1268
|
for (const plugin of reportArray(nodeStats, "pluginsData", "PluginsData")) {
|
|
1140
1269
|
const pluginName = asString(reportValue(plugin, "pluginName", "PluginName"));
|
|
1141
1270
|
const hints = buildDotnetPluginHints(plugin);
|
|
@@ -1169,7 +1298,7 @@ function buildDotnetHtmlTabs(nodeStats) {
|
|
|
1169
1298
|
body = buildDotnetGroupedCorrelationSummaryHtml(bodyRows, `grouped-correlation-${tabs.length}`);
|
|
1170
1299
|
}
|
|
1171
1300
|
else if (lowerPlugin.includes("correlation") && lowerTable.includes("ungrouped correlation rows")) {
|
|
1172
|
-
title = "Ungrouped
|
|
1301
|
+
title = "Ungrouped Correlation Summary";
|
|
1173
1302
|
body = buildDotnetUngroupedCorrelationSummaryHtml(bodyRows, `ungrouped-correlation-${tabs.length}`);
|
|
1174
1303
|
}
|
|
1175
1304
|
tabs.push([`plugin-${tabs.length}`, title, `${hints}${body}`]);
|
|
@@ -1285,9 +1414,9 @@ function buildDotnetMarkdownReport(nodeStats) {
|
|
|
1285
1414
|
*/
|
|
1286
1415
|
function buildDotnetHtmlReport(nodeStats) {
|
|
1287
1416
|
const tabs = buildDotnetHtmlTabs(nodeStats);
|
|
1288
|
-
const buttonsHtml = tabs.map(([tabId, title]) => `<button class="tab-btn" data-tab="${tabId}">${escapeHtml(title)}</button>${REPORT_EOL}`).join("");
|
|
1289
|
-
const sectionsHtml = tabs.map(([tabId, , html]) => `<section id="${tabId}" class="tab">${html}</section>${REPORT_EOL}`).join("");
|
|
1290
|
-
const chartDataJson = JSON.stringify(buildDotnetChartData(nodeStats));
|
|
1417
|
+
const buttonsHtml = tabs.map(([tabId, title]) => `<button class="tab-btn" data-tab="${escapeHtml(tabId)}">${escapeHtml(title)}</button>${REPORT_EOL}`).join("");
|
|
1418
|
+
const sectionsHtml = tabs.map(([tabId, , html]) => `<section id="${escapeHtml(tabId)}" class="tab">${html}</section>${REPORT_EOL}`).join("");
|
|
1419
|
+
const chartDataJson = escapeJsonForHtmlScript(JSON.stringify(buildDotnetChartData(nodeStats)));
|
|
1291
1420
|
const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
|
|
1292
1421
|
const template = `<!doctype html>
|
|
1293
1422
|
<html lang="en">
|
|
@@ -1379,7 +1508,7 @@ const reportLogo=document.querySelector('[data-report-logo]');
|
|
|
1379
1508
|
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
1509
|
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
1510
|
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(
|
|
1511
|
+
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
1512
|
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
1513
|
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
1514
|
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;}}
|