@loadstrike/loadstrike-sdk 1.0.30401 → 1.0.31601
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 +6 -0
- package/dist/cjs/iteration-observation-diagnostics.js +513 -0
- package/dist/cjs/iteration-observations.js +195 -22
- package/dist/cjs/local-report-input.js +21 -0
- package/dist/cjs/local.js +55 -10
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +436 -140
- package/dist/cjs/runtime.js +313 -85
- package/dist/cjs/sink-retry-policy.js +52 -0
- package/dist/cjs/sinks.js +112 -9
- package/dist/cjs/transports.js +78 -25
- package/dist/esm/iteration-observation-diagnostics.js +508 -0
- package/dist/esm/iteration-observations.js +195 -22
- package/dist/esm/local-report-input.js +17 -0
- package/dist/esm/local.js +55 -10
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +436 -140
- package/dist/esm/runtime.js +313 -85
- package/dist/esm/sink-retry-policy.js +44 -0
- package/dist/esm/sinks.js +112 -9
- package/dist/esm/transports.js +78 -25
- package/dist/types/iteration-observation-diagnostics.d.ts +21 -0
- package/dist/types/iteration-observations.d.ts +5 -0
- package/dist/types/local-report-input.d.ts +6 -0
- package/dist/types/report-history.d.ts +124 -0
- package/dist/types/reporting-svg.d.ts +2 -0
- package/dist/types/reporting.d.ts +6 -3
- package/dist/types/runtime.d.ts +25 -0
- package/dist/types/sink-retry-policy.d.ts +9 -0
- package/dist/types/sinks.d.ts +6 -0
- package/dist/types/transports.d.ts +4 -0
- package/package.json +2 -2
package/dist/cjs/reporting.js
CHANGED
|
@@ -8,6 +8,9 @@ exports.buildDotnetHtmlReport = buildDotnetHtmlReport;
|
|
|
8
8
|
const node_fs_1 = require("node:fs");
|
|
9
9
|
const node_path_1 = require("node:path");
|
|
10
10
|
const node_url_1 = require("node:url");
|
|
11
|
+
const report_history_js_1 = require("./report-history.js");
|
|
12
|
+
const local_report_input_js_1 = require("./local-report-input.js");
|
|
13
|
+
const reporting_svg_js_1 = require("./reporting-svg.js");
|
|
11
14
|
const REPORT_EOL = "\n";
|
|
12
15
|
const REPORT_FALLBACK_SVG = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='#64b5ff'/><stop offset='1' stop-color='#2f66db'/></linearGradient></defs><rect width='128' height='128' rx='24' fill='#081325'/><path d='M24 94L56 24l16 34 14-22 18 58h-16l-7-23-9 13-10-21-14 31H24z' fill='url(#g)'/></svg>";
|
|
13
16
|
const REPORT_LOGO_CACHE = new Map();
|
|
@@ -45,8 +48,12 @@ function reportValue(source, ...keys) {
|
|
|
45
48
|
}
|
|
46
49
|
const record = source;
|
|
47
50
|
for (const key of keys) {
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
52
|
+
if (descriptor
|
|
53
|
+
&& "value" in descriptor
|
|
54
|
+
&& descriptor.value !== undefined
|
|
55
|
+
&& descriptor.value !== null) {
|
|
56
|
+
return descriptor.value;
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
return undefined;
|
|
@@ -291,6 +298,40 @@ function loadStrikeNodeTypeTag(value) {
|
|
|
291
298
|
return asInt(value);
|
|
292
299
|
}
|
|
293
300
|
}
|
|
301
|
+
function loadStrikeNodeTypeLabel(value) {
|
|
302
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
303
|
+
const record = value;
|
|
304
|
+
if (record.tag !== undefined || record.Tag !== undefined) {
|
|
305
|
+
return ["Single node", "Coordinator", "Agent"][loadStrikeNodeTypeTag(value)]
|
|
306
|
+
?? "Unknown";
|
|
307
|
+
}
|
|
308
|
+
return "Unknown";
|
|
309
|
+
}
|
|
310
|
+
const text = asString(value).trim();
|
|
311
|
+
return {
|
|
312
|
+
SingleNode: "Single node",
|
|
313
|
+
Coordinator: "Coordinator",
|
|
314
|
+
Agent: "Agent",
|
|
315
|
+
"0": "Single node",
|
|
316
|
+
"1": "Coordinator",
|
|
317
|
+
"2": "Agent"
|
|
318
|
+
}[text] ?? (text || "Unknown");
|
|
319
|
+
}
|
|
320
|
+
function loadStrikeRuntimeVersionLabel(nodeInfo) {
|
|
321
|
+
for (const [camelKey, pascalKey] of [
|
|
322
|
+
["runtimeVersion", "RuntimeVersion"],
|
|
323
|
+
["pythonVersion", "PythonVersion"],
|
|
324
|
+
["nodeVersion", "NodeVersion"],
|
|
325
|
+
["dotNetVersion", "DotNetVersion"],
|
|
326
|
+
["engineVersion", "EngineVersion"]
|
|
327
|
+
]) {
|
|
328
|
+
const value = asString(reportValue(nodeInfo, camelKey, pascalKey)).trim();
|
|
329
|
+
if (value) {
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return "Unavailable";
|
|
334
|
+
}
|
|
294
335
|
function parseUtcDate(value) {
|
|
295
336
|
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
296
337
|
return value;
|
|
@@ -329,7 +370,22 @@ function formatDotnetDateTime(value) {
|
|
|
329
370
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${fraction}Z`;
|
|
330
371
|
}
|
|
331
372
|
function escapeJsonForHtmlScript(value) {
|
|
332
|
-
return value.
|
|
373
|
+
return value.replace(/[<>&\u2028\u2029]/g, (character) => {
|
|
374
|
+
switch (character) {
|
|
375
|
+
case "<":
|
|
376
|
+
return "\\u003c";
|
|
377
|
+
case ">":
|
|
378
|
+
return "\\u003e";
|
|
379
|
+
case "&":
|
|
380
|
+
return "\\u0026";
|
|
381
|
+
case "\u2028":
|
|
382
|
+
return "\\u2028";
|
|
383
|
+
case "\u2029":
|
|
384
|
+
return "\\u2029";
|
|
385
|
+
default:
|
|
386
|
+
return character;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
333
389
|
}
|
|
334
390
|
function buildReportLogoDataUri(resourceName) {
|
|
335
391
|
const cached = REPORT_LOGO_CACHE.get(resourceName);
|
|
@@ -405,6 +461,9 @@ function buildDotnetTableHtml(rows, wrapInCard = true) {
|
|
|
405
461
|
return parts.join("");
|
|
406
462
|
}
|
|
407
463
|
function formatReportTableHeader(header) {
|
|
464
|
+
if (header === "UnmatchedDestination") {
|
|
465
|
+
return "Unmatched Destination";
|
|
466
|
+
}
|
|
408
467
|
if (header === "LatencyStdDev") {
|
|
409
468
|
return "LatencyStdDev (ms)";
|
|
410
469
|
}
|
|
@@ -502,21 +561,14 @@ function buildFailedEventRows(plugins) {
|
|
|
502
561
|
}
|
|
503
562
|
function tryParseReportFloat(value) {
|
|
504
563
|
const parsed = Number.parseFloat(asString(value));
|
|
505
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
506
|
-
}
|
|
507
|
-
function meanNumeric(values) {
|
|
508
|
-
if (!values.length) {
|
|
509
|
-
return 0;
|
|
510
|
-
}
|
|
511
|
-
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
564
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
512
565
|
}
|
|
513
|
-
function
|
|
514
|
-
if (
|
|
566
|
+
function percentileFromOrdered(values, percentileValue) {
|
|
567
|
+
if (values.length === 0) {
|
|
515
568
|
return 0;
|
|
516
569
|
}
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
return ordered[index];
|
|
570
|
+
const index = Math.min(Math.max(Math.ceil(percentileValue * values.length) - 1, 0), values.length - 1);
|
|
571
|
+
return values[index];
|
|
520
572
|
}
|
|
521
573
|
function buildGroupedCorrelationChartPayloads(rows) {
|
|
522
574
|
const grouped = new Map();
|
|
@@ -534,33 +586,48 @@ function buildGroupedCorrelationChartPayloads(rows) {
|
|
|
534
586
|
}
|
|
535
587
|
return [...grouped.values()]
|
|
536
588
|
.filter((group) => group.rows.some((row) => tryParseReportFloat(row.LatencyP50Ms) !== undefined
|
|
589
|
+
|| tryParseReportFloat(row.LatencyP75Ms) !== undefined
|
|
537
590
|
|| tryParseReportFloat(row.LatencyP80Ms) !== undefined
|
|
538
591
|
|| tryParseReportFloat(row.LatencyP85Ms) !== undefined
|
|
539
592
|
|| tryParseReportFloat(row.LatencyP90Ms) !== undefined
|
|
540
593
|
|| tryParseReportFloat(row.LatencyP95Ms) !== undefined
|
|
541
|
-
|| tryParseReportFloat(row.LatencyP99Ms) !== undefined
|
|
594
|
+
|| tryParseReportFloat(row.LatencyP99Ms) !== undefined
|
|
595
|
+
|| tryParseReportFloat(row.LatencyMaxMs ?? row.LatencyMax) !== undefined))
|
|
542
596
|
.sort((left, right) => left.key[0] === right.key[0]
|
|
543
597
|
? left.key[1].localeCompare(right.key[1])
|
|
544
598
|
: left.key[0].localeCompare(right.key[0]))
|
|
545
599
|
.map((group, index) => ({
|
|
546
600
|
title: `${group.key[0]}: ${group.key[1]}`,
|
|
547
|
-
subtitle: group.rows.length > 1
|
|
601
|
+
subtitle: group.rows.length > 1
|
|
602
|
+
? `${group.rows.length} distinct scenario and destination rows.`
|
|
603
|
+
: "Single grouped row.",
|
|
548
604
|
chart: {
|
|
549
|
-
labels: ["P50", "P80", "P85", "P90", "P95", "P99"],
|
|
550
|
-
series: [
|
|
551
|
-
{
|
|
552
|
-
|
|
553
|
-
|
|
605
|
+
labels: ["P50", "P75", "P80", "P85", "P90", "P95", "P99", "Max"],
|
|
606
|
+
series: [...group.rows]
|
|
607
|
+
.sort((left, right) => {
|
|
608
|
+
const leftIdentity = `${readReportRowText(left, "Scenario")}|${readReportRowText(left, "Destination")}`;
|
|
609
|
+
const rightIdentity = `${readReportRowText(right, "Scenario")}|${readReportRowText(right, "Destination")}`;
|
|
610
|
+
return leftIdentity.localeCompare(rightIdentity);
|
|
611
|
+
})
|
|
612
|
+
.map((row, rowIndex) => {
|
|
613
|
+
const scenario = readReportRowText(row, "Scenario") || "<scenario unavailable>";
|
|
614
|
+
const destination = readReportRowText(row, "Destination") || "<destination unavailable>";
|
|
615
|
+
const name = `${scenario} | ${destination}${rowIndex > 0 ? ` (row ${rowIndex + 1})` : ""}`;
|
|
616
|
+
return {
|
|
617
|
+
name,
|
|
618
|
+
color: ["#38bdf8", "#22c55e", "#f59e0b", "#a855f7", "#f43f5e", "#06b6d4", "#14b8a6", "#eab308", "#818cf8", "#84cc16"][(index + rowIndex) % 10],
|
|
554
619
|
values: [
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
620
|
+
tryParseReportFloat(row.LatencyP50Ms) ?? null,
|
|
621
|
+
tryParseReportFloat(row.LatencyP75Ms) ?? null,
|
|
622
|
+
tryParseReportFloat(row.LatencyP80Ms) ?? null,
|
|
623
|
+
tryParseReportFloat(row.LatencyP85Ms) ?? null,
|
|
624
|
+
tryParseReportFloat(row.LatencyP90Ms) ?? null,
|
|
625
|
+
tryParseReportFloat(row.LatencyP95Ms) ?? null,
|
|
626
|
+
tryParseReportFloat(row.LatencyP99Ms) ?? null,
|
|
627
|
+
tryParseReportFloat(row.LatencyMaxMs ?? row.LatencyMax) ?? null
|
|
561
628
|
]
|
|
562
|
-
}
|
|
563
|
-
|
|
629
|
+
};
|
|
630
|
+
})
|
|
564
631
|
}
|
|
565
632
|
}));
|
|
566
633
|
}
|
|
@@ -583,7 +650,6 @@ function buildUngroupedCorrelationChartPayload(rows) {
|
|
|
583
650
|
grouped.set(key, { scenario, destination, statusCode, latencies: [latency] });
|
|
584
651
|
}
|
|
585
652
|
}
|
|
586
|
-
const nameCount = new Map();
|
|
587
653
|
const series = [...grouped.values()]
|
|
588
654
|
.sort((left, right) => {
|
|
589
655
|
const leftKey = `${left.scenario}|${left.destination}|${left.statusCode}`;
|
|
@@ -591,25 +657,24 @@ function buildUngroupedCorrelationChartPayload(rows) {
|
|
|
591
657
|
return leftKey.localeCompare(rightKey);
|
|
592
658
|
})
|
|
593
659
|
.map((row, index) => {
|
|
594
|
-
const
|
|
595
|
-
const key = baseName.toLowerCase();
|
|
596
|
-
const count = nameCount.get(key) ?? 0;
|
|
597
|
-
nameCount.set(key, count + 1);
|
|
660
|
+
const orderedLatencies = [...row.latencies].sort((left, right) => left - right);
|
|
598
661
|
return {
|
|
599
|
-
name:
|
|
662
|
+
name: `${row.scenario} | ${row.destination} | status ${row.statusCode}`,
|
|
600
663
|
color: ["#38bdf8", "#22c55e", "#f59e0b", "#a855f7", "#f43f5e", "#06b6d4", "#14b8a6", "#eab308", "#818cf8", "#84cc16"][index % 10],
|
|
601
664
|
values: [
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
665
|
+
percentileFromOrdered(orderedLatencies, 0.50),
|
|
666
|
+
percentileFromOrdered(orderedLatencies, 0.75),
|
|
667
|
+
percentileFromOrdered(orderedLatencies, 0.80),
|
|
668
|
+
percentileFromOrdered(orderedLatencies, 0.85),
|
|
669
|
+
percentileFromOrdered(orderedLatencies, 0.90),
|
|
670
|
+
percentileFromOrdered(orderedLatencies, 0.95),
|
|
671
|
+
percentileFromOrdered(orderedLatencies, 0.99),
|
|
672
|
+
orderedLatencies[orderedLatencies.length - 1]
|
|
608
673
|
]
|
|
609
674
|
};
|
|
610
675
|
});
|
|
611
676
|
return {
|
|
612
|
-
labels: ["P50", "P80", "P85", "P90", "P95", "P99"],
|
|
677
|
+
labels: ["P50", "P75", "P80", "P85", "P90", "P95", "P99", "Max"],
|
|
613
678
|
series
|
|
614
679
|
};
|
|
615
680
|
}
|
|
@@ -745,8 +810,34 @@ function hasMeasurementData(measurement) {
|
|
|
745
810
|
asString(reportValue(code, "statusCode", "StatusCode")).trim().length > 0 ||
|
|
746
811
|
asString(reportValue(code, "message", "Message")).trim().length > 0);
|
|
747
812
|
}
|
|
748
|
-
function appendChartCard(parts, id, title) {
|
|
749
|
-
|
|
813
|
+
function appendChartCard(parts, id, title, source, kind, unit, seriesLabel, compatibilityClass = "", extraHostAttributes = "") {
|
|
814
|
+
const cardClass = compatibilityClass.includes("correlation")
|
|
815
|
+
? "chart-card correlation-chart-card"
|
|
816
|
+
: "chart-card";
|
|
817
|
+
appendReportLine(parts, `<div class="${cardClass}" data-chart-title="${escapeHtml(title)}"><h3>${escapeHtml(title)}</h3>`);
|
|
818
|
+
appendReportLine(parts, "<div class=\"chart-actions\" aria-label=\"Chart controls\">");
|
|
819
|
+
for (const [action, label] of [
|
|
820
|
+
["zoom-in", "Zoom in"],
|
|
821
|
+
["zoom-out", "Zoom out"],
|
|
822
|
+
["pan-left", "Pan left"],
|
|
823
|
+
["pan-right", "Pan right"],
|
|
824
|
+
["reset", "Reset"],
|
|
825
|
+
["expand", "Expand"]
|
|
826
|
+
]) {
|
|
827
|
+
appendReportLine(parts, `<button type="button" class="chart-action" data-chart-action="${action}" aria-label="${label}">${label}</button>`);
|
|
828
|
+
}
|
|
829
|
+
appendReportLine(parts, "</div>");
|
|
830
|
+
appendReportLine(parts, `<div class="chart-host" data-loadstrike-chart-engine="svg-v2" data-chart-source="${escapeHtml(source)}" data-chart-kind="${escapeHtml(kind)}" data-chart-unit="${escapeHtml(unit)}" data-series-label="${escapeHtml(seriesLabel)}" data-chart-title="${escapeHtml(title)}"${extraHostAttributes ? ` ${extraHostAttributes}` : ""}>`);
|
|
831
|
+
appendReportLine(parts, `<svg id="${escapeHtml(id)}" class="chart-canvas${compatibilityClass ? ` ${escapeHtml(compatibilityClass)}` : ""}" role="img" aria-label="${escapeHtml(title)}" tabindex="0" viewBox="0 0 720 320" preserveAspectRatio="xMidYMid meet"><title>${escapeHtml(title)}</title></svg>`);
|
|
832
|
+
appendReportLine(parts, "<div class=\"chart-tooltip\" data-chart-tooltip role=\"status\" aria-live=\"polite\" hidden></div>");
|
|
833
|
+
appendReportLine(parts, "<div class=\"chart-legend\" data-chart-legend aria-label=\"Chart legend\"></div>");
|
|
834
|
+
appendReportLine(parts, "</div></div>");
|
|
835
|
+
}
|
|
836
|
+
function appendChartCollectionTools(parts) {
|
|
837
|
+
appendReportLine(parts, "<div class=\"chart-collection-tools\">");
|
|
838
|
+
appendReportLine(parts, "<label>Search charts<input type=\"search\" data-chart-search placeholder=\"Filter by chart title\" /></label>");
|
|
839
|
+
appendReportLine(parts, "<label>Chart size<select data-chart-grid-size><option value=\"compact\">Compact</option><option value=\"comfortable\" selected>Comfortable</option><option value=\"spacious\">Spacious</option></select></label>");
|
|
840
|
+
appendReportLine(parts, "</div>");
|
|
750
841
|
}
|
|
751
842
|
function hasChartPointData(points) {
|
|
752
843
|
return Array.isArray(points) && points.length > 0;
|
|
@@ -757,7 +848,7 @@ function hasPieChartData(points) {
|
|
|
757
848
|
function hasLatencyTrendData(chart) {
|
|
758
849
|
const labels = reportArray(chart, "labels", "Labels");
|
|
759
850
|
const series = reportArray(chart, "series", "Series");
|
|
760
|
-
return labels.length > 0 && series.some((entry) => reportArray(entry, "values", "Values").some((value) => Number.isFinite(
|
|
851
|
+
return labels.length > 0 && series.some((entry) => reportArray(entry, "values", "Values").some((value) => value !== null && value !== undefined && Number.isFinite(Number(value)) && Number(value) >= 0));
|
|
761
852
|
}
|
|
762
853
|
function hasNonEmptyHints(plugin) {
|
|
763
854
|
return reportArray(plugin, "hints", "Hints").some((hint) => asString(hint).trim().length > 0);
|
|
@@ -900,11 +991,169 @@ function buildDotnetStatusCodeClassChart(scenarios) {
|
|
|
900
991
|
{ label: "Other", value: buckets.Other, color: "#8b5cf6" }
|
|
901
992
|
].filter((entry) => entry.value > 0);
|
|
902
993
|
}
|
|
903
|
-
function
|
|
994
|
+
function genuineCombinedMeasurement(source) {
|
|
995
|
+
const value = reportValue(source, "allMeasurement", "AllMeasurement");
|
|
996
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
997
|
+
? value
|
|
998
|
+
: undefined;
|
|
999
|
+
}
|
|
1000
|
+
function measurementHasObservations(measurement) {
|
|
1001
|
+
if (!measurement) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
const request = reportObject(measurement, "request", "Request");
|
|
1005
|
+
if (asInt(reportValue(request, "count", "Count")) > 0) {
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
const count64 = asString(reportValue(measurement, "count64", "Count64")).trim();
|
|
1009
|
+
return /^\d+$/.test(count64) && /[1-9]/.test(count64);
|
|
1010
|
+
}
|
|
1011
|
+
function validMeasurementLatency(measurement, camelKey, pascalKey) {
|
|
1012
|
+
if (!measurement) {
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
if (!measurementHasObservations(measurement)) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
const value = Number(reportValue(reportObject(measurement, "latency", "Latency"), camelKey, pascalKey));
|
|
1019
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
1020
|
+
}
|
|
1021
|
+
function measurementUsesApproximateHistogram(measurement) {
|
|
1022
|
+
if (!measurement) {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
const mode = asString(reportValue(measurement, "distributionMode", "DistributionMode")).toLocaleLowerCase();
|
|
1026
|
+
const maxRelativeError = asFloat(reportValue(measurement, "maxRelativeError", "MaxRelativeError"));
|
|
1027
|
+
return maxRelativeError > 0
|
|
1028
|
+
|| mode.includes("quantized")
|
|
1029
|
+
|| mode.includes("approx");
|
|
1030
|
+
}
|
|
1031
|
+
function buildOutcomeLatencySeries(scenarios, percentileName, camelKey, pascalKey) {
|
|
1032
|
+
return [
|
|
1033
|
+
{
|
|
1034
|
+
outcome: "All",
|
|
1035
|
+
color: "#8b5cf6",
|
|
1036
|
+
measurement: (scenario) => genuineCombinedMeasurement(scenario)
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
outcome: "OK",
|
|
1040
|
+
color: "#18a957",
|
|
1041
|
+
measurement: (scenario) => reportObject(scenario, "ok", "Ok")
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
outcome: "Failed",
|
|
1045
|
+
color: "#d14343",
|
|
1046
|
+
measurement: (scenario) => reportObject(scenario, "fail", "Fail")
|
|
1047
|
+
}
|
|
1048
|
+
].map((candidate) => {
|
|
1049
|
+
const measurements = scenarios.map(candidate.measurement);
|
|
1050
|
+
const values = measurements.map((measurement) => validMeasurementLatency(measurement, camelKey, pascalKey));
|
|
1051
|
+
const approximate = measurements.some((measurement, index) => values[index] !== null && measurementUsesApproximateHistogram(measurement));
|
|
1052
|
+
return {
|
|
1053
|
+
name: `${candidate.outcome} ${percentileName}${approximate ? " (approx.)" : ""}`,
|
|
1054
|
+
color: candidate.color,
|
|
1055
|
+
values
|
|
1056
|
+
};
|
|
1057
|
+
}).filter((series) => series.values.some((value) => value !== null));
|
|
1058
|
+
}
|
|
1059
|
+
function buildHistoryChartData(points) {
|
|
1060
|
+
if (!points.length) {
|
|
1061
|
+
return {
|
|
1062
|
+
cumulativeRequestsHistory: { labels: [], series: [] },
|
|
1063
|
+
achievedRequestRateHistory: { labels: [], series: [] },
|
|
1064
|
+
cumulativeBytesHistory: { labels: [], series: [] },
|
|
1065
|
+
historyLatencyCharts: []
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const ordered = [...points].sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
|
|
1069
|
+
const labels = ordered.map((point) => `${Number(point.elapsedSeconds.toFixed(3))}s`);
|
|
1070
|
+
const scenarioNames = [...new Set(ordered.flatMap((point) => [...point.scenarios]
|
|
1071
|
+
.sort((left, right) => (left.sortIndex ?? 0) - (right.sortIndex ?? 0))
|
|
1072
|
+
.map((scenario) => scenario.scenarioName)))];
|
|
1073
|
+
const palette = ["#38bdf8", "#22c55e", "#f59e0b", "#a855f7", "#f43f5e", "#14b8a6", "#eab308", "#818cf8", "#06b6d4", "#84cc16"];
|
|
1074
|
+
const findScenario = (point, scenarioName) => point.scenarios.find((scenario) => scenario.scenarioName === scenarioName);
|
|
1075
|
+
const totalCount = (scenario) => scenario?.all?.count ?? ((scenario?.ok?.count ?? 0) + (scenario?.failed?.count ?? 0));
|
|
1076
|
+
const totalBytes = (scenario) => scenario?.all?.bytes ?? ((scenario?.ok?.bytes ?? 0) + (scenario?.failed?.bytes ?? 0));
|
|
1077
|
+
const cumulativeRequestsHistory = {
|
|
1078
|
+
labels,
|
|
1079
|
+
series: scenarioNames.map((scenarioName, index) => ({
|
|
1080
|
+
name: scenarioName,
|
|
1081
|
+
color: palette[index % palette.length],
|
|
1082
|
+
values: ordered.map((point) => {
|
|
1083
|
+
const scenario = findScenario(point, scenarioName);
|
|
1084
|
+
return scenario ? totalCount(scenario) : null;
|
|
1085
|
+
})
|
|
1086
|
+
}))
|
|
1087
|
+
};
|
|
1088
|
+
const cumulativeBytesHistory = {
|
|
1089
|
+
labels,
|
|
1090
|
+
series: scenarioNames.map((scenarioName, index) => ({
|
|
1091
|
+
name: scenarioName,
|
|
1092
|
+
color: palette[index % palette.length],
|
|
1093
|
+
values: ordered.map((point) => {
|
|
1094
|
+
const scenario = findScenario(point, scenarioName);
|
|
1095
|
+
return scenario ? totalBytes(scenario) : null;
|
|
1096
|
+
})
|
|
1097
|
+
}))
|
|
1098
|
+
};
|
|
1099
|
+
const ratesByScenario = new Map((0, report_history_js_1.deriveScenarioRates)(ordered).map((series) => [series.scenarioName, series.values]));
|
|
1100
|
+
const achievedRequestRateHistory = {
|
|
1101
|
+
labels,
|
|
1102
|
+
series: scenarioNames.map((scenarioName, index) => ({
|
|
1103
|
+
name: scenarioName,
|
|
1104
|
+
color: palette[index % palette.length],
|
|
1105
|
+
values: ratesByScenario.get(scenarioName) ?? labels.map(() => null)
|
|
1106
|
+
}))
|
|
1107
|
+
};
|
|
1108
|
+
const outcomes = [
|
|
1109
|
+
["All", (scenario) => scenario.all],
|
|
1110
|
+
["OK", (scenario) => scenario.ok],
|
|
1111
|
+
["Failed", (scenario) => scenario.failed]
|
|
1112
|
+
];
|
|
1113
|
+
const percentiles = [
|
|
1114
|
+
["P50", "percent50Ms"],
|
|
1115
|
+
["P75", "percent75Ms"],
|
|
1116
|
+
["P95", "percent95Ms"],
|
|
1117
|
+
["P99", "percent99Ms"]
|
|
1118
|
+
];
|
|
1119
|
+
const historyLatencyCharts = scenarioNames.map((scenarioName, scenarioIndex) => {
|
|
1120
|
+
let seriesIndex = 0;
|
|
1121
|
+
const series = outcomes.flatMap(([outcomeName, measurementSelector]) => percentiles.map(([percentileName, percentileKey]) => {
|
|
1122
|
+
const measurements = ordered.map((point) => {
|
|
1123
|
+
const scenario = findScenario(point, scenarioName);
|
|
1124
|
+
return scenario ? measurementSelector(scenario) : undefined;
|
|
1125
|
+
});
|
|
1126
|
+
const values = measurements.map((measurement) => {
|
|
1127
|
+
const value = measurement?.[percentileKey];
|
|
1128
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
1129
|
+
});
|
|
1130
|
+
const approximate = measurements.some((measurement, index) => values[index] !== null && measurement?.approximate === true);
|
|
1131
|
+
return {
|
|
1132
|
+
name: `${outcomeName} ${percentileName}${approximate ? " (approx.)" : ""}`,
|
|
1133
|
+
color: palette[(scenarioIndex + seriesIndex++) % palette.length],
|
|
1134
|
+
values
|
|
1135
|
+
};
|
|
1136
|
+
}).filter((series) => series.values.some((value) => value !== null)));
|
|
1137
|
+
return {
|
|
1138
|
+
title: `Cumulative Latency - ${scenarioName}`,
|
|
1139
|
+
chart: { labels, series }
|
|
1140
|
+
};
|
|
1141
|
+
}).filter((item) => item.chart.series.length > 0);
|
|
1142
|
+
return {
|
|
1143
|
+
cumulativeRequestsHistory,
|
|
1144
|
+
achievedRequestRateHistory,
|
|
1145
|
+
cumulativeBytesHistory,
|
|
1146
|
+
historyLatencyCharts
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function buildDotnetChartData(nodeStats, localReportInput) {
|
|
904
1150
|
const scenarios = reportScenarios(nodeStats);
|
|
905
|
-
const
|
|
906
|
-
.map((scenario) => ({ scenario, measurement:
|
|
1151
|
+
const genuineCombinedScenarios = scenarios
|
|
1152
|
+
.map((scenario) => ({ scenario, measurement: genuineCombinedMeasurement(scenario) }))
|
|
907
1153
|
.filter((item) => item.measurement !== undefined);
|
|
1154
|
+
const history = localReportInput.history.status === "available"
|
|
1155
|
+
? buildHistoryChartData(localReportInput.history.points)
|
|
1156
|
+
: buildHistoryChartData([]);
|
|
908
1157
|
return {
|
|
909
1158
|
overallOutcome: [
|
|
910
1159
|
{ label: "OK", value: reportTotalOkCount(nodeStats, scenarios), color: "#18a957" },
|
|
@@ -915,11 +1164,15 @@ function buildDotnetChartData(nodeStats) {
|
|
|
915
1164
|
value: reportRequestCountValue(scenario),
|
|
916
1165
|
color: "#3b82f6"
|
|
917
1166
|
})),
|
|
918
|
-
scenarioP95Latency:
|
|
1167
|
+
scenarioP95Latency: genuineCombinedScenarios.map(({ scenario, measurement }) => ({
|
|
919
1168
|
label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
|
|
920
1169
|
value: asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95")),
|
|
921
1170
|
color: "#8b5cf6"
|
|
922
1171
|
})),
|
|
1172
|
+
scenarioP95LatencyByOutcome: {
|
|
1173
|
+
labels: scenarios.map((scenario) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
|
|
1174
|
+
series: buildOutcomeLatencySeries(scenarios, "P95", "percent95", "Percent95")
|
|
1175
|
+
},
|
|
923
1176
|
scenarioRps: scenarios.map((scenario) => ({
|
|
924
1177
|
label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
|
|
925
1178
|
value: reportDurationSeconds(reportDurationValue(scenario)) <= 0
|
|
@@ -941,17 +1194,18 @@ function buildDotnetChartData(nodeStats) {
|
|
|
941
1194
|
})),
|
|
942
1195
|
statusCodeClasses: buildDotnetStatusCodeClassChart(scenarios),
|
|
943
1196
|
scenarioLatencyTrend: {
|
|
944
|
-
labels:
|
|
1197
|
+
labels: scenarios.map((scenario) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
|
|
945
1198
|
series: [
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1199
|
+
...buildOutcomeLatencySeries(scenarios, "P50", "percent50", "Percent50"),
|
|
1200
|
+
...buildOutcomeLatencySeries(scenarios, "P75", "percent75", "Percent75"),
|
|
1201
|
+
...buildOutcomeLatencySeries(scenarios, "P95", "percent95", "Percent95"),
|
|
1202
|
+
...buildOutcomeLatencySeries(scenarios, "P99", "percent99", "Percent99")
|
|
950
1203
|
]
|
|
951
|
-
}
|
|
1204
|
+
},
|
|
1205
|
+
...history
|
|
952
1206
|
};
|
|
953
1207
|
}
|
|
954
|
-
function buildDotnetSummaryHtml(nodeStats) {
|
|
1208
|
+
function buildDotnetSummaryHtml(nodeStats, localReportInput) {
|
|
955
1209
|
const scenarios = reportScenarios(nodeStats);
|
|
956
1210
|
const allRequests = reportTotalRequestCount(nodeStats, scenarios);
|
|
957
1211
|
const allOk = reportTotalOkCount(nodeStats, scenarios);
|
|
@@ -973,11 +1227,19 @@ function buildDotnetSummaryHtml(nodeStats) {
|
|
|
973
1227
|
? scenario
|
|
974
1228
|
: winner;
|
|
975
1229
|
}, undefined);
|
|
976
|
-
const latencyRows =
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1230
|
+
const latencyRows = [];
|
|
1231
|
+
for (const scenario of scenarios) {
|
|
1232
|
+
const scenarioName = asString(reportValue(scenario, "scenarioName", "ScenarioName"));
|
|
1233
|
+
const ok = reportObject(scenario, "ok", "Ok");
|
|
1234
|
+
const fail = reportObject(scenario, "fail", "Fail");
|
|
1235
|
+
if (measurementHasObservations(ok)) {
|
|
1236
|
+
latencyRows.push(buildSummaryLatencyRow(scenarioName, "OK", ok));
|
|
1237
|
+
}
|
|
1238
|
+
if (measurementHasObservations(fail)) {
|
|
1239
|
+
latencyRows.push(buildSummaryLatencyRow(scenarioName, "FAIL", fail));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const chartData = buildDotnetChartData(nodeStats, localReportInput);
|
|
981
1243
|
const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
|
|
982
1244
|
const nodeInfo = reportObject(nodeStats, "nodeInfo", "NodeInfo");
|
|
983
1245
|
const totalBytes = reportTotalBytes(nodeStats, scenarios);
|
|
@@ -990,39 +1252,62 @@ function buildDotnetSummaryHtml(nodeStats) {
|
|
|
990
1252
|
appendReportLine(parts, `<div class="stat-card"><div class="stat-label">Duration</div><div class="stat-value">${escapeHtml(formatDotnetTimeSpan(reportDurationValue(nodeStats)))}</div></div>`);
|
|
991
1253
|
appendReportLine(parts, `<div class="stat-card"><div class="stat-label">Total Bytes</div><div class="stat-value">${totalBytes}</div></div>`);
|
|
992
1254
|
appendReportLine(parts, `<div class="stat-card"><div class="stat-label">Top Scenario</div><div class="stat-value">${escapeHtml(topScenario ? reportValue(topScenario, "scenarioName", "ScenarioName") : "n/a")}</div></div>`);
|
|
993
|
-
appendReportLine(parts, `<div class="stat-card"><div class="stat-label">Node</div><div class="stat-value">${
|
|
1255
|
+
appendReportLine(parts, `<div class="stat-card"><div class="stat-label">Node</div><div class="stat-value">${escapeHtml(loadStrikeNodeTypeLabel(reportValue(nodeInfo, "nodeType", "NodeType")))}</div></div>`);
|
|
994
1256
|
appendReportLine(parts, "</div>");
|
|
1257
|
+
const hasApproximateLatencySeries = [
|
|
1258
|
+
...reportArray(reportObject(chartData, "scenarioP95LatencyByOutcome"), "series"),
|
|
1259
|
+
...reportArray(reportObject(chartData, "scenarioLatencyTrend"), "series"),
|
|
1260
|
+
...reportArray(chartData, "historyLatencyCharts")
|
|
1261
|
+
.flatMap((item) => reportArray(reportObject(item, "chart"), "series"))
|
|
1262
|
+
].some((series) => asString(reportValue(series, "name")).endsWith(" (approx.)"));
|
|
1263
|
+
if (hasApproximateLatencySeries) {
|
|
1264
|
+
appendReportLine(parts, "<p class=\"meta-note\">Series marked '(approx.)' use bounded native histogram estimates; completed request and transferred-byte totals remain exact.</p>");
|
|
1265
|
+
}
|
|
995
1266
|
const charts = [];
|
|
996
1267
|
if (hasPieChartData(chartData.overallOutcome)) {
|
|
997
|
-
appendChartCard(charts, "chart-outcome", "Success vs Fail");
|
|
1268
|
+
appendChartCard(charts, "chart-outcome", "Success vs Fail", "overallOutcome", "donut", "requests", "Outcome");
|
|
998
1269
|
}
|
|
999
1270
|
if (hasChartPointData(chartData.scenarioRequests)) {
|
|
1000
|
-
appendChartCard(charts, "chart-scenario-requests", "Requests by Scenario");
|
|
1271
|
+
appendChartCard(charts, "chart-scenario-requests", "Requests by Scenario", "scenarioRequests", "bar", "requests", "Requests");
|
|
1001
1272
|
}
|
|
1002
|
-
if (
|
|
1003
|
-
appendChartCard(charts, "chart-scenario-p95", "P95 Latency by Scenario (ms)");
|
|
1273
|
+
if (hasLatencyTrendData(chartData.scenarioP95LatencyByOutcome)) {
|
|
1274
|
+
appendChartCard(charts, "chart-scenario-p95", "P95 Latency by Scenario (ms)", "scenarioP95LatencyByOutcome", "bar", "ms", "P95");
|
|
1004
1275
|
}
|
|
1005
1276
|
if (hasChartPointData(chartData.scenarioRps)) {
|
|
1006
|
-
appendChartCard(charts, "chart-scenario-rps", "RPS by Scenario");
|
|
1277
|
+
appendChartCard(charts, "chart-scenario-rps", "RPS by Scenario", "scenarioRps", "bar", "req/s", "Rate");
|
|
1007
1278
|
}
|
|
1008
1279
|
if (hasChartPointData(chartData.scenarioFailRate)) {
|
|
1009
|
-
appendChartCard(charts, "chart-scenario-fail-rate", "Failure Rate by Scenario (%)");
|
|
1280
|
+
appendChartCard(charts, "chart-scenario-fail-rate", "Failure Rate by Scenario (%)", "scenarioFailRate", "bar", "%", "Failure rate");
|
|
1010
1281
|
}
|
|
1011
1282
|
if (hasChartPointData(chartData.scenarioBytes)) {
|
|
1012
|
-
appendChartCard(charts, "chart-scenario-bytes", "Bytes by Scenario");
|
|
1283
|
+
appendChartCard(charts, "chart-scenario-bytes", "Bytes by Scenario", "scenarioBytes", "bar", "bytes", "Transferred");
|
|
1013
1284
|
}
|
|
1014
1285
|
if (hasPieChartData(chartData.statusCodeClasses)) {
|
|
1015
|
-
appendChartCard(charts, "chart-status-code-classes", "Status Code Class Mix");
|
|
1286
|
+
appendChartCard(charts, "chart-status-code-classes", "Status Code Class Mix", "statusCodeClasses", "donut", "responses", "Status");
|
|
1016
1287
|
}
|
|
1017
1288
|
if (hasLatencyTrendData(chartData.scenarioLatencyTrend)) {
|
|
1018
|
-
appendChartCard(charts, "chart-scenario-latency-lines", "Latency
|
|
1289
|
+
appendChartCard(charts, "chart-scenario-latency-lines", "Latency Percentile Profiles by Scenario (ms)", "scenarioLatencyTrend", "line", "ms", "Latency");
|
|
1290
|
+
}
|
|
1291
|
+
if (hasLatencyTrendData(chartData.cumulativeRequestsHistory)) {
|
|
1292
|
+
appendChartCard(charts, "chart-history-requests", "Cumulative Completed Requests by Scenario", "cumulativeRequestsHistory", "line", "requests", "Requests");
|
|
1293
|
+
}
|
|
1294
|
+
if (hasLatencyTrendData(chartData.achievedRequestRateHistory)) {
|
|
1295
|
+
appendChartCard(charts, "chart-history-rate", "Achieved Request Rate by Scenario", "achievedRequestRateHistory", "line", "req/s", "Rate");
|
|
1296
|
+
}
|
|
1297
|
+
if (hasLatencyTrendData(chartData.cumulativeBytesHistory)) {
|
|
1298
|
+
appendChartCard(charts, "chart-history-bytes", "Cumulative Transferred Bytes by Scenario", "cumulativeBytesHistory", "line", "bytes", "Transferred");
|
|
1299
|
+
}
|
|
1300
|
+
for (const [historyIndex, historyChart] of chartData.historyLatencyCharts.entries()) {
|
|
1301
|
+
appendChartCard(charts, `chart-history-latency-${historyIndex}`, asString(historyChart.title), "historyLatencyCharts", "line", "ms", "Latency", "", `data-chart-index="${historyIndex}"`);
|
|
1019
1302
|
}
|
|
1020
1303
|
if (charts.length > 0) {
|
|
1021
|
-
appendReportLine(parts, "<div class=\"card\">");
|
|
1304
|
+
appendReportLine(parts, "<div class=\"card chart-collection\" data-chart-collection data-grid-size=\"comfortable\">");
|
|
1022
1305
|
appendReportLine(parts, "<h2>Charts</h2>");
|
|
1306
|
+
appendChartCollectionTools(parts);
|
|
1023
1307
|
appendReportLine(parts, "<div class=\"chart-grid\">");
|
|
1024
1308
|
parts.push(...charts);
|
|
1025
1309
|
appendReportLine(parts, "</div>");
|
|
1310
|
+
appendReportLine(parts, "<div class=\"chart-empty\" data-chart-empty>No charts match this search.</div>");
|
|
1026
1311
|
appendReportLine(parts, "</div>");
|
|
1027
1312
|
}
|
|
1028
1313
|
if (latencyRows.length > 0) {
|
|
@@ -1039,7 +1324,7 @@ function buildDotnetSummaryHtml(nodeStats) {
|
|
|
1039
1324
|
appendReportLine(parts, `<tr><th>Created (UTC)</th><td>${escapeHtml(formatDotnetDateTime(reportValue(testInfo, "createdUtc", "CreatedUtc", "created", "Created")))}</td></tr>`);
|
|
1040
1325
|
appendReportLine(parts, `<tr><th>Machine</th><td>${escapeHtml(reportValue(nodeInfo, "machineName", "MachineName"))}</td></tr>`);
|
|
1041
1326
|
appendReportLine(parts, `<tr><th>OS</th><td>${escapeHtml(reportValue(nodeInfo, "os", "OS"))}</td></tr>`);
|
|
1042
|
-
appendReportLine(parts, `<tr><th>
|
|
1327
|
+
appendReportLine(parts, `<tr><th>Runtime</th><td>${escapeHtml(loadStrikeRuntimeVersionLabel(nodeInfo))}</td></tr>`);
|
|
1043
1328
|
appendReportLine(parts, `<tr><th>Processor</th><td>${escapeHtml(reportValue(nodeInfo, "processor", "Processor"))}</td></tr>`);
|
|
1044
1329
|
appendReportLine(parts, `<tr><th>Cores</th><td>${asInt(reportValue(nodeInfo, "coresCount", "CoresCount"))}</td></tr>`);
|
|
1045
1330
|
appendReportLine(parts, `<tr><th>Operation</th><td>${escapeHtml(reportValue(nodeInfo, "currentOperation", "CurrentOperation"))}</td></tr>`);
|
|
@@ -1091,8 +1376,30 @@ function buildDotnetThresholdHtml(nodeStats) {
|
|
|
1091
1376
|
function buildDotnetMetricHtml(nodeStats) {
|
|
1092
1377
|
return buildDotnetTableHtml(buildDotnetMetricRows(nodeStats));
|
|
1093
1378
|
}
|
|
1094
|
-
function
|
|
1095
|
-
|
|
1379
|
+
function reportHistoryUnavailableReason(localReportInput) {
|
|
1380
|
+
if (localReportInput.history.status === "available") {
|
|
1381
|
+
return "";
|
|
1382
|
+
}
|
|
1383
|
+
switch (localReportInput.history.reasonCategory) {
|
|
1384
|
+
case "capture_failure":
|
|
1385
|
+
return "History capture became unavailable.";
|
|
1386
|
+
case "budget_pressure":
|
|
1387
|
+
return "History was omitted because the report history budget was exceeded.";
|
|
1388
|
+
case "distributed_temporal_aggregation_unavailable":
|
|
1389
|
+
return "Temporal aggregation is unavailable for distributed results.";
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function buildDotnetGeneratorDeliveryHtml(nodeStats, localReportInput = (0, local_report_input_js_1.emptyLocalReportInput)()) {
|
|
1393
|
+
const warnings = [
|
|
1394
|
+
...reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings"),
|
|
1395
|
+
...(localReportInput.history.status === "unavailable"
|
|
1396
|
+
? [{
|
|
1397
|
+
code: "report_history_unavailable",
|
|
1398
|
+
count64: "1",
|
|
1399
|
+
reasonCategory: localReportInput.history.reasonCategory
|
|
1400
|
+
}]
|
|
1401
|
+
: [])
|
|
1402
|
+
];
|
|
1096
1403
|
const stats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
|
|
1097
1404
|
const topLevelSegments = reportArray(nodeStats, "schedulerSegments", "SchedulerSegments");
|
|
1098
1405
|
const segments = topLevelSegments.length > 0
|
|
@@ -1107,6 +1414,9 @@ function buildDotnetGeneratorDeliveryHtml(nodeStats) {
|
|
|
1107
1414
|
appendReportLine(parts, "<div class=\"card\">");
|
|
1108
1415
|
appendReportLine(parts, "<h2>Generator Delivery</h2>");
|
|
1109
1416
|
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>");
|
|
1417
|
+
if (localReportInput.history.status === "unavailable") {
|
|
1418
|
+
appendReportLine(parts, `<p>${escapeHtml(reportHistoryUnavailableReason(localReportInput))}</p>`);
|
|
1419
|
+
}
|
|
1110
1420
|
appendReportLine(parts, "<div class=\"card-grid\">");
|
|
1111
1421
|
for (const [label, value] of [
|
|
1112
1422
|
["Configured Max In Flight", asInt(reportValue(stats, "configuredMaxInFlight", "ConfiguredMaxInFlight"))],
|
|
@@ -1123,7 +1433,7 @@ function buildDotnetGeneratorDeliveryHtml(nodeStats) {
|
|
|
1123
1433
|
}
|
|
1124
1434
|
appendReportLine(parts, "</div></div>");
|
|
1125
1435
|
if (warnings.length) {
|
|
1126
|
-
appendReportLine(parts, "<div class=\"card\"><h2>Generator Warnings</h2>");
|
|
1436
|
+
appendReportLine(parts, "<div class=\"card\"><h2>Generator and Reporting Warnings</h2>");
|
|
1127
1437
|
parts.push(buildDotnetTableHtml(warnings, false));
|
|
1128
1438
|
appendReportLine(parts, "</div>");
|
|
1129
1439
|
}
|
|
@@ -1148,7 +1458,7 @@ function buildDotnetGeneratorDeliveryHtml(nodeStats) {
|
|
|
1148
1458
|
}
|
|
1149
1459
|
return parts.join("");
|
|
1150
1460
|
}
|
|
1151
|
-
function hasDotnetGeneratorDeliveryData(nodeStats) {
|
|
1461
|
+
function hasDotnetGeneratorDeliveryData(nodeStats, localReportInput = (0, local_report_input_js_1.emptyLocalReportInput)()) {
|
|
1152
1462
|
const schedulerStats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
|
|
1153
1463
|
const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
|
|
1154
1464
|
const hasNonZeroDecimal = (value) => {
|
|
@@ -1156,6 +1466,7 @@ function hasDotnetGeneratorDeliveryData(nodeStats) {
|
|
|
1156
1466
|
return text.trim().length > 0 && text !== "0";
|
|
1157
1467
|
};
|
|
1158
1468
|
return reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings").length > 0
|
|
1469
|
+
|| localReportInput.history.status === "unavailable"
|
|
1159
1470
|
|| reportArray(nodeStats, "schedulerSegments", "SchedulerSegments").length > 0
|
|
1160
1471
|
|| reportArray(schedulerStats, "segments", "Segments").length > 0
|
|
1161
1472
|
|| asInt(reportValue(schedulerStats, "configuredMaxInFlight", "ConfiguredMaxInFlight")) > 0
|
|
@@ -1175,15 +1486,19 @@ function buildDotnetGroupedCorrelationSummaryHtml(rows, groupedChartKey) {
|
|
|
1175
1486
|
return parts.join("");
|
|
1176
1487
|
}
|
|
1177
1488
|
appendReportLine(parts);
|
|
1178
|
-
appendReportLine(parts, "<div class=\"card\"><h2>Grouped Correlation Percentile Trends</h2><p>Each chart is one GatherBy value.
|
|
1489
|
+
appendReportLine(parts, "<div class=\"card\"><h2>Grouped Correlation Percentile Trends</h2><p>Each chart is one GatherBy value. Every scenario and destination row remains a separate percentile series from P50 through Max.</p></div>");
|
|
1490
|
+
appendReportLine(parts, "<div class=\"card chart-collection\" data-chart-collection data-grid-size=\"comfortable\">");
|
|
1491
|
+
appendChartCollectionTools(parts);
|
|
1179
1492
|
appendReportLine(parts, "<div class=\"chart-grid correlation-chart-grid\">");
|
|
1180
1493
|
payloads.forEach((payload, index) => {
|
|
1181
1494
|
const chartKey = `${groupedChartKey}-value-${index}`;
|
|
1182
1495
|
const chartJson = escapeJsonForHtmlScript(JSON.stringify(payload.chart));
|
|
1183
1496
|
appendReportLine(parts, `<script type="application/json" data-grouped-correlation-chart="${escapeHtml(chartKey)}">${chartJson}</script>`);
|
|
1184
|
-
|
|
1497
|
+
appendChartCard(parts, `${chartKey}-chart`, payload.title, "grouped-correlation", "line", "ms", "Latency", "grouped-correlation-canvas", `data-grouped-key="${escapeHtml(chartKey)}"`);
|
|
1185
1498
|
});
|
|
1186
1499
|
appendReportLine(parts, "</div>");
|
|
1500
|
+
appendReportLine(parts, "<div class=\"chart-empty\" data-chart-empty>No charts match this search.</div>");
|
|
1501
|
+
appendReportLine(parts, "</div>");
|
|
1187
1502
|
return parts.join("");
|
|
1188
1503
|
}
|
|
1189
1504
|
function buildDotnetUngroupedCorrelationSummaryHtml(rows, groupedChartKey) {
|
|
@@ -1195,8 +1510,12 @@ function buildDotnetUngroupedCorrelationSummaryHtml(rows, groupedChartKey) {
|
|
|
1195
1510
|
appendReportLine(parts);
|
|
1196
1511
|
appendReportLine(parts, "<div class=\"card\"><h2>Ungrouped Correlation Percentile Trends</h2><p>All percentile lines are shown in one graph for direct comparison.</p></div>");
|
|
1197
1512
|
appendReportLine(parts, `<script type="application/json" data-ungrouped-correlation="${escapeHtml(groupedChartKey)}">${chartJson}</script>`);
|
|
1513
|
+
appendReportLine(parts, "<div class=\"card chart-collection\" data-chart-collection data-grid-size=\"comfortable\">");
|
|
1514
|
+
appendChartCollectionTools(parts);
|
|
1198
1515
|
appendReportLine(parts, "<div class=\"chart-grid correlation-chart-grid\">");
|
|
1199
|
-
|
|
1516
|
+
appendChartCard(parts, `${groupedChartKey}-chart`, "Ungrouped Correlation Percentiles", "ungrouped-correlation", "line", "ms", "Latency", "ungrouped-correlation-canvas", `data-ungrouped-key="${escapeHtml(groupedChartKey)}"`);
|
|
1517
|
+
appendReportLine(parts, "</div>");
|
|
1518
|
+
appendReportLine(parts, "<div class=\"chart-empty\" data-chart-empty>No charts match this search.</div>");
|
|
1200
1519
|
appendReportLine(parts, "</div>");
|
|
1201
1520
|
}
|
|
1202
1521
|
return parts.join("");
|
|
@@ -1208,8 +1527,8 @@ function buildDotnetPluginHints(plugin) {
|
|
|
1208
1527
|
const hints = reportArray(plugin, "hints", "Hints").map((hint) => asString(hint).trim()).filter((hint) => hint.length > 0);
|
|
1209
1528
|
return `<div class="card"><strong>Hints</strong><ul>${hints.map((hint) => `<li>${escapeHtml(hint)}</li>`).join("")}</ul></div>`;
|
|
1210
1529
|
}
|
|
1211
|
-
function buildDotnetHtmlTabs(nodeStats) {
|
|
1212
|
-
const tabs = [["summary", "Summary", buildDotnetSummaryHtml(nodeStats)]];
|
|
1530
|
+
function buildDotnetHtmlTabs(nodeStats, localReportInput = (0, local_report_input_js_1.emptyLocalReportInput)()) {
|
|
1531
|
+
const tabs = [["summary", "Summary", buildDotnetSummaryHtml(nodeStats, localReportInput)]];
|
|
1213
1532
|
const scenarioRows = buildDotnetScenarioRows(nodeStats);
|
|
1214
1533
|
if (scenarioRows.length) {
|
|
1215
1534
|
tabs.push(["scenarios", "Scenarios", buildDotnetTableHtml(scenarioRows)]);
|
|
@@ -1243,8 +1562,8 @@ function buildDotnetHtmlTabs(nodeStats) {
|
|
|
1243
1562
|
if (metricRows.length) {
|
|
1244
1563
|
tabs.push(["metrics", "Metrics", buildDotnetTableHtml(metricRows)]);
|
|
1245
1564
|
}
|
|
1246
|
-
if (hasDotnetGeneratorDeliveryData(nodeStats)) {
|
|
1247
|
-
tabs.push(["generator-delivery", "Generator Delivery", buildDotnetGeneratorDeliveryHtml(nodeStats)]);
|
|
1565
|
+
if (hasDotnetGeneratorDeliveryData(nodeStats, localReportInput)) {
|
|
1566
|
+
tabs.push(["generator-delivery", "Generator Delivery", buildDotnetGeneratorDeliveryHtml(nodeStats, localReportInput)]);
|
|
1248
1567
|
}
|
|
1249
1568
|
for (const plugin of reportArray(nodeStats, "pluginsData", "PluginsData")) {
|
|
1250
1569
|
const pluginName = asString(reportValue(plugin, "pluginName", "PluginName"));
|
|
@@ -1279,7 +1598,7 @@ function buildDotnetHtmlTabs(nodeStats) {
|
|
|
1279
1598
|
body = buildDotnetGroupedCorrelationSummaryHtml(bodyRows, `grouped-correlation-${tabs.length}`);
|
|
1280
1599
|
}
|
|
1281
1600
|
else if (lowerPlugin.includes("correlation") && lowerTable.includes("ungrouped correlation rows")) {
|
|
1282
|
-
title = "Ungrouped
|
|
1601
|
+
title = "Ungrouped Correlation Summary";
|
|
1283
1602
|
body = buildDotnetUngroupedCorrelationSummaryHtml(bodyRows, `ungrouped-correlation-${tabs.length}`);
|
|
1284
1603
|
}
|
|
1285
1604
|
tabs.push([`plugin-${tabs.length}`, title, `${hints}${body}`]);
|
|
@@ -1391,41 +1710,47 @@ function buildDotnetMarkdownReport(nodeStats) {
|
|
|
1391
1710
|
return reportLines(lines);
|
|
1392
1711
|
}
|
|
1393
1712
|
/**
|
|
1394
|
-
*
|
|
1713
|
+
* Builds the portable single-file HTML report. The optional second argument is
|
|
1714
|
+
* an internal report-only carrier used by the native runner and is never added
|
|
1715
|
+
* to public result, sink, observation, or cluster payloads.
|
|
1395
1716
|
*/
|
|
1396
|
-
function buildDotnetHtmlReport(nodeStats) {
|
|
1397
|
-
const tabs = buildDotnetHtmlTabs(nodeStats);
|
|
1398
|
-
const buttonsHtml = tabs
|
|
1399
|
-
|
|
1400
|
-
|
|
1717
|
+
function buildDotnetHtmlReport(nodeStats, localReportInput = (0, local_report_input_js_1.emptyLocalReportInput)()) {
|
|
1718
|
+
const tabs = buildDotnetHtmlTabs(nodeStats, localReportInput);
|
|
1719
|
+
const buttonsHtml = tabs
|
|
1720
|
+
.map(([tabId, title]) => `<button class="tab-btn" data-tab="${escapeHtml(tabId)}">${escapeHtml(title)}</button>${REPORT_EOL}`)
|
|
1721
|
+
.join("");
|
|
1722
|
+
const sectionsHtml = tabs
|
|
1723
|
+
.map(([tabId, , html]) => `<section id="${escapeHtml(tabId)}" class="tab">${html}</section>${REPORT_EOL}`)
|
|
1724
|
+
.join("");
|
|
1725
|
+
const chartDataJson = escapeJsonForHtmlScript(JSON.stringify(buildDotnetChartData(nodeStats, localReportInput)));
|
|
1401
1726
|
const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
|
|
1402
1727
|
const template = `<!doctype html>
|
|
1403
1728
|
<html lang="en">
|
|
1404
1729
|
<head>
|
|
1405
1730
|
<meta charset="utf-8" />
|
|
1406
1731
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1732
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; object-src 'none'; frame-src 'none'; form-action 'none'; base-uri 'none'" />
|
|
1407
1733
|
<title>LoadStrike Report</title>
|
|
1408
1734
|
<style>
|
|
1409
|
-
:root{--bg:#f4f7fc;--bgTop:#fff3d0;--bgRight:#dbe8ff;--bgBottom:#edf2fb;--panel:#ffffff;--panelAlt:#eef3fb;--line:#d3deed;--text:#0f172a;--muted:#4b5563;--accent:#2563eb;--ok:#138a4a;--fail:#c63636;--warn:#b7791f;--chip:#edf3fc;--card:#ffffff;--stat:#f5f9ff;--chartPanel:#0f172a;--
|
|
1410
|
-
body[data-theme='dark']{--bg:#0d1117;--bgTop:#18243b;--bgRight:#102235;--bgBottom:#0b0f14;--panel:#111827;--panelAlt:#0f172a;--line:#263241;--text:#e6edf3;--muted:#9fb0c3;--accent:#2563eb;--ok:#18a957;--fail:#d14343;--warn:#f59e0b;--chip:rgba(31,41,55,.45);--card:linear-gradient(180deg,rgba(17,24,39,.92) 0,rgba(13,19,32,.92) 100%);--stat:rgba(20,30,47,.65);--chartPanel:rgba(15,23,42,.72);--
|
|
1735
|
+
:root{--bg:#f4f7fc;--bgTop:#fff3d0;--bgRight:#dbe8ff;--bgBottom:#edf2fb;--panel:#ffffff;--panelAlt:#eef3fb;--line:#d3deed;--text:#0f172a;--muted:#4b5563;--accent:#2563eb;--ok:#138a4a;--fail:#c63636;--warn:#b7791f;--chip:#edf3fc;--card:#ffffff;--stat:#f5f9ff;--chartPanel:#0f172a;--chartLegendText:#e6edf3;--shadow:0 10px 24px rgba(15,23,42,.1)}
|
|
1736
|
+
body[data-theme='dark']{--bg:#0d1117;--bgTop:#18243b;--bgRight:#102235;--bgBottom:#0b0f14;--panel:#111827;--panelAlt:#0f172a;--line:#263241;--text:#e6edf3;--muted:#9fb0c3;--accent:#2563eb;--ok:#18a957;--fail:#d14343;--warn:#f59e0b;--chip:rgba(31,41,55,.45);--card:linear-gradient(180deg,rgba(17,24,39,.92) 0,rgba(13,19,32,.92) 100%);--stat:rgba(20,30,47,.65);--chartPanel:rgba(15,23,42,.72);--chartLegendText:#e6edf3;--shadow:0 10px 24px rgba(0,0,0,.2)}
|
|
1411
1737
|
body{margin:0;background:radial-gradient(1200px 700px at 10% -10%,var(--bgTop) 0,transparent 58%),radial-gradient(900px 620px at 100% 0,var(--bgRight) 0,transparent 57%),var(--bgBottom);color:var(--text);font-family:'Segoe UI',Tahoma,sans-serif}
|
|
1412
1738
|
.wrap{max-width:1440px;margin:0 auto;padding:20px}
|
|
1413
|
-
.report-brand{display:flex;align-items:center;gap:16px;margin:0 0 10px
|
|
1739
|
+
.report-brand{display:flex;align-items:center;gap:16px;margin:0 0 10px;padding-top:8px;overflow:visible;flex-wrap:wrap}
|
|
1414
1740
|
.report-logo-slot{width:380px;height:184px;max-width:100%;flex:0 0 auto;display:flex;align-items:flex-start;justify-content:flex-start;overflow:visible}
|
|
1415
|
-
.report-logo{width:100%;height:100%;object-fit:contain;object-position:left top;border:
|
|
1741
|
+
.report-logo{width:100%;height:100%;object-fit:contain;object-position:left top;border:0;background:transparent;box-shadow:none;border-radius:0;padding:0;margin:0;overflow:visible;transform:scale(var(--report-logo-scale,1));transform-origin:left top;transition:transform .2s ease}
|
|
1416
1742
|
body[data-theme='dark'] .report-logo{--report-logo-scale:1.175}
|
|
1417
1743
|
.theme-toggle-report{position:fixed;top:12px;right:12px;z-index:1200;display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;appearance:none;border:1px solid var(--line);background:var(--chip);color:var(--text);padding:0;border-radius:999px;font-size:18px;font-weight:700;cursor:pointer;transition:all .2s ease}
|
|
1418
1744
|
.theme-toggle-report:hover{border-color:var(--accent);transform:translateY(-1px)}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
h3{margin:0 0 10px 0;font-size:15px;color:var(--text)}
|
|
1745
|
+
h1{margin:0 0 10px;font-size:28px;letter-spacing:.2px}
|
|
1746
|
+
h2{margin:0 0 12px;font-size:18px}
|
|
1747
|
+
h3{margin:0 0 10px;font-size:15px;color:var(--text)}
|
|
1423
1748
|
.meta{display:flex;gap:14px;flex-wrap:wrap;color:var(--muted);font-size:13px;margin-bottom:16px}
|
|
1424
1749
|
.meta span{background:var(--chip);border:1px solid var(--line);padding:6px 10px;border-radius:999px}
|
|
1425
1750
|
.report-layout{display:grid;grid-template-columns:280px minmax(0,1fr);gap:14px;align-items:start}
|
|
1426
1751
|
.tabs-pane{position:sticky;top:12px;max-height:calc(100vh - 24px);overflow:auto;overscroll-behavior:contain;padding-right:4px;cursor:grab}
|
|
1427
1752
|
.tabs-pane.panning{cursor:grabbing;user-select:none}
|
|
1428
|
-
.tabs{display:flex;flex-direction:column;gap:8px;margin:12px 0 14px
|
|
1753
|
+
.tabs{display:flex;flex-direction:column;gap:8px;margin:12px 0 14px}
|
|
1429
1754
|
.tab-btn{background:var(--panelAlt);color:var(--text);border:1px solid var(--line);padding:8px 12px;border-radius:8px;cursor:pointer;transition:all .2s ease;text-align:left;width:100%}
|
|
1430
1755
|
.tab-btn:hover{border-color:var(--accent);background:var(--chip)}
|
|
1431
1756
|
.tab-btn.active{background:linear-gradient(180deg,#2f66db 0,#2754b8 100%);border-color:#3f73e0}
|
|
@@ -1444,22 +1769,19 @@ th{background:var(--panelAlt);position:sticky;top:0;z-index:1}
|
|
|
1444
1769
|
.value-fail{color:var(--fail)}
|
|
1445
1770
|
.chart-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:12px}
|
|
1446
1771
|
.chart-card{padding:12px;border:1px solid var(--line);border-radius:10px;background:var(--chartPanel)}
|
|
1447
|
-
.chart-
|
|
1448
|
-
.correlation-chart-grid{grid-template-columns:repeat(auto-fit,minmax(420px,720px));justify-content:center}
|
|
1772
|
+
.correlation-chart-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,420px),720px));justify-content:center}
|
|
1449
1773
|
.correlation-chart-card{max-width:720px;width:100%}
|
|
1450
|
-
.correlation-chart-card .chart-canvas{height:320px}
|
|
1451
1774
|
.chart-card h3,.chart-card p{color:var(--chartLegendText)}
|
|
1452
1775
|
.table-wrap{overflow:auto;max-height:70vh}
|
|
1453
|
-
|
|
1776
|
+
${reporting_svg_js_1.REPORT_SVG_CSS}
|
|
1777
|
+
@media (max-width:980px){.wrap{padding:14px}.report-brand{margin-bottom:8px;padding-top:4px}.report-logo-slot{width:300px;height:146px}.theme-toggle-report{top:10px;right:10px}.report-layout{grid-template-columns:1fr}.tabs-pane{position:static;max-height:none;cursor:auto}.tabs{flex-direction:row;flex-wrap:wrap}.tab-btn{width:auto}.correlation-chart-grid{grid-template-columns:1fr;justify-content:stretch}.correlation-chart-card{max-width:none}.stat-value{font-size:18px}}
|
|
1454
1778
|
</style>
|
|
1455
1779
|
</head>
|
|
1456
1780
|
<body data-theme="light">
|
|
1457
1781
|
<div class="wrap">
|
|
1458
1782
|
<button type="button" class="theme-toggle-report" data-report-theme-toggle aria-label="Switch to dark theme" aria-pressed="false" title="Switch to dark theme"><span aria-hidden="true">☾</span></button>
|
|
1459
1783
|
<div class="report-brand">
|
|
1460
|
-
<div class="report-logo-slot">
|
|
1461
|
-
<img src="__LOGO_LIGHT__" alt="LoadStrike logo" class="report-logo" data-report-logo data-logo-light="__LOGO_LIGHT__" data-logo-dark="__LOGO_DARK__" />
|
|
1462
|
-
</div>
|
|
1784
|
+
<div class="report-logo-slot"><img src="__LOGO_LIGHT__" alt="LoadStrike logo" class="report-logo" data-report-logo data-logo-light="__LOGO_LIGHT__" data-logo-dark="__LOGO_DARK__" /></div>
|
|
1463
1785
|
<h1>LoadStrike Report</h1>
|
|
1464
1786
|
</div>
|
|
1465
1787
|
<div class="meta">
|
|
@@ -1469,44 +1791,18 @@ th{background:var(--panelAlt);position:sticky;top:0;z-index:1}
|
|
|
1469
1791
|
<span>Duration: <strong>__DURATION__</strong></span>
|
|
1470
1792
|
</div>
|
|
1471
1793
|
<div class="report-layout">
|
|
1472
|
-
<aside class="tabs-pane" id="tab-pane">
|
|
1473
|
-
|
|
1474
|
-
__BUTTONS__</div>
|
|
1475
|
-
</aside>
|
|
1794
|
+
<aside class="tabs-pane" id="tab-pane"><div class="tabs">
|
|
1795
|
+
__BUTTONS__</div></aside>
|
|
1476
1796
|
<div class="tabs-content">
|
|
1477
1797
|
__SECTIONS__</div>
|
|
1478
1798
|
</div>
|
|
1479
1799
|
</div>
|
|
1800
|
+
<div class="chart-modal" data-chart-fullscreen-overlay role="dialog" aria-modal="true" aria-label="Expanded LoadStrike chart" aria-hidden="true">
|
|
1801
|
+
<div class="chart-modal-panel"><div class="chart-modal-head"><button type="button" class="chart-modal-close" data-chart-modal-close aria-label="Close expanded chart">Close</button></div><div data-chart-modal-panel></div></div>
|
|
1802
|
+
</div>
|
|
1480
1803
|
<script>
|
|
1481
1804
|
const reportCharts=__CHART_DATA__;
|
|
1482
|
-
|
|
1483
|
-
const tabSections=[...document.querySelectorAll('.tab')];
|
|
1484
|
-
const tabsPane=document.getElementById('tab-pane');
|
|
1485
|
-
const linePalette=['#38bdf8','#22c55e','#f59e0b','#a855f7','#f43f5e','#14b8a6','#eab308','#818cf8','#06b6d4','#84cc16'];
|
|
1486
|
-
const reportThemeKey='loadstrike-report-theme';
|
|
1487
|
-
const reportThemeToggle=document.querySelector('[data-report-theme-toggle]');
|
|
1488
|
-
const reportLogo=document.querySelector('[data-report-logo]');
|
|
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');}}
|
|
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));}
|
|
1491
|
-
function formatMetric(v){if(!Number.isFinite(v))return '0';return Math.abs(v)>=100?v.toFixed(0):v.toFixed(2);}
|
|
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};}
|
|
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);}
|
|
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);}}
|
|
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;}}
|
|
1496
|
-
function drawLatencyLine(canvasRef,chart){const canvas=typeof canvasRef==='string'?document.getElementById(canvasRef):canvasRef;if(!canvas)return;const c=setupCanvas(canvas);const ctx=c.ctx,w=c.w,h=c.h;ctx.clearRect(0,0,w,h);if(!chart||!Array.isArray(chart.labels)||chart.labels.length===0||!Array.isArray(chart.series)||chart.series.length===0){drawNoData(ctx,w,h,'No latency data');return;}const labels=chart.labels;const seriesList=chart.series;const allValues=seriesList.flatMap(s=>(s.values||[]).filter(v=>Number.isFinite(v)));if(allValues.length===0){drawNoData(ctx,w,h,'No latency data');return;}const shortAxisLabels=labels.every(label=>((label||'').toString().length<=4));const rotateAxisLabels=!shortAxisLabels&&labels.length>4;const left=52,right=18,bottom=rotateAxisLabels?78:52;const legendItemWidth=150,legendLineHeight=14,legendTop=12;const plotWidth=Math.max(120,w-left-right);const legendCols=Math.max(1,Math.floor(plotWidth/legendItemWidth));const legendRows=Math.max(1,Math.ceil(seriesList.length/legendCols));const legendHeight=legendRows*legendLineHeight;const top=legendTop+legendHeight+16;const pw=w-left-right;const ph=h-top-bottom;if(ph<=20){drawNoData(ctx,w,h,'No latency data');return;}const max=Math.max(...allValues,1);const scaleMax=max*1.08;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();}ctx.fillStyle='#b5c2d3';ctx.font='11px Segoe UI';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);}const xStep=labels.length<=1?0:pw/(labels.length-1);const xAt=i=>labels.length<=1?left+(pw/2):left+(xStep*i);const overlapOffset=new Map();const epsilon=Math.max(max*0.0005,0.001);for(let pointIndex=0;pointIndex<labels.length;pointIndex++){const points=[];for(let seriesIndex=0;seriesIndex<seriesList.length;seriesIndex++){const value=(seriesList[seriesIndex].values||[])[pointIndex];if(Number.isFinite(value)){points.push({seriesIndex,value});}}points.sort((a,b)=>a.value===b.value?a.seriesIndex-b.seriesIndex:a.value-b.value);let start=0;while(start<points.length){let end=start+1;while(end<points.length&&Math.abs(points[end].value-points[start].value)<=epsilon){end++;}const count=end-start;if(count>1){const mid=(count-1)/2;for(let k=0;k<count;k++){overlapOffset.set(points[start+k].seriesIndex+'|'+pointIndex,(k-mid)*3);}}start=end;}}const dashPatterns=[[0,0],[7,4],[2,3],[10,3,2,3]];for(let seriesIndex=0;seriesIndex<seriesList.length;seriesIndex++){const series=seriesList[seriesIndex];ctx.beginPath();ctx.strokeStyle=series.color||'#38bdf8';ctx.lineWidth=2.2;const dash=dashPatterns[seriesIndex%dashPatterns.length];if(dash[0]===0){ctx.setLineDash([]);}else{ctx.setLineDash(dash);}let started=false;(series.values||[]).forEach((value,index)=>{if(!Number.isFinite(value)){started=false;return;}const x=xAt(index);const offset=overlapOffset.get(seriesIndex+'|'+index)||0;const y=top+ph-((value/scaleMax)*ph)+offset;if(!started){ctx.moveTo(x,y);started=true;}else{ctx.lineTo(x,y);}});ctx.stroke();ctx.setLineDash([]);(series.values||[]).forEach((value,index)=>{if(!Number.isFinite(value))return;const x=xAt(index);const offset=overlapOffset.get(seriesIndex+'|'+index)||0;const y=top+ph-((value/scaleMax)*ph)+offset;ctx.beginPath();ctx.fillStyle='#0f172a';ctx.arc(x,y,3.2,0,Math.PI*2);ctx.fill();ctx.beginPath();ctx.strokeStyle=series.color||'#38bdf8';ctx.lineWidth=2;ctx.arc(x,y,3.2,0,Math.PI*2);ctx.stroke();});}ctx.fillStyle='#b5c2d3';ctx.font='10px Segoe UI';ctx.textAlign='center';labels.forEach((label,index)=>{const x=xAt(index);const text=(label||'').toString().slice(0,34);if(rotateAxisLabels){ctx.save();ctx.translate(x,h-bottom+12);ctx.rotate(-0.6);ctx.fillText(text,0,0);ctx.restore();}else{ctx.fillText(text,x,h-bottom+16);}});let lx=left,ly=legendTop+2;for(let seriesIndex=0;seriesIndex<seriesList.length;seriesIndex++){const series=seriesList[seriesIndex];const rawName=(series.name||'Series').toString();const legendName=rawName.length>30?rawName.slice(0,27)+'...':rawName;ctx.strokeStyle=series.color||'#38bdf8';ctx.lineWidth=2.2;const dash=dashPatterns[seriesIndex%dashPatterns.length];if(dash[0]===0){ctx.setLineDash([]);}else{ctx.setLineDash(dash);}ctx.beginPath();ctx.moveTo(lx,ly+1.5);ctx.lineTo(lx+12,ly+1.5);ctx.stroke();ctx.setLineDash([]);ctx.fillStyle='#b5c2d3';ctx.font='11px Segoe UI';ctx.textAlign='left';ctx.fillText(legendName,lx+16,ly+4);lx+=legendItemWidth;if(lx>w-right-legendItemWidth){lx=left;ly+=legendLineHeight;}}}
|
|
1497
|
-
function renderCharts(){drawPie('chart-outcome',reportCharts.overallOutcome);drawBar('chart-scenario-requests',reportCharts.scenarioRequests);drawBar('chart-scenario-p95',reportCharts.scenarioP95Latency);drawBar('chart-scenario-rps',reportCharts.scenarioRps);drawBar('chart-scenario-fail-rate',reportCharts.scenarioFailRate);drawBar('chart-scenario-bytes',reportCharts.scenarioBytes);drawPie('chart-status-code-classes',reportCharts.statusCodeClasses);drawLatencyLine('chart-scenario-latency-lines',reportCharts.scenarioLatencyTrend);}
|
|
1498
|
-
function getGroupedCorrelationChart(key){const node=document.querySelector('script[type="application/json"][data-grouped-correlation-chart="'+key+'"]');if(!node)return {labels:[],series:[]};try{return JSON.parse(node.textContent||'{"labels":[],"series":[]}');}catch{return {labels:[],series:[]};}}
|
|
1499
|
-
function getUngroupedCorrelationChart(key){const node=document.querySelector('script[type="application/json"][data-ungrouped-correlation="'+key+'"]');if(!node)return {labels:[],series:[]};try{return JSON.parse(node.textContent||'{"labels":[],"series":[]}');}catch{return {labels:[],series:[]};}}
|
|
1500
|
-
function normalizeUngroupedCorrelationChart(chart){if(!chart||!Array.isArray(chart.labels)||!Array.isArray(chart.series))return {labels:[],series:[]};const compactName=input=>{const raw=(input||'Series').toString();const parts=raw.split('|').map(x=>x.trim()).filter(x=>x.length>0);const tail=parts.length>0?parts[parts.length-1]:raw.trim();if(tail.length===0)return 'Series';return tail.length>22?tail.slice(0,19)+'...':tail;};const labels=chart.labels||[];const series=chart.series||[];const normalizedSeries=series.map((item,index)=>({name:compactName(item&&item.name),color:(item&&item.color)||linePalette[index%linePalette.length],values:Array.isArray(item&&item.values)?item.values.map(v=>Number.isFinite(v)?v:NaN):[]})).filter(s=>s.values.some(v=>Number.isFinite(v)));const isPercentile=value=>/^p\\d+$/i.test((value||'').toString().trim());const labelsArePercentiles=labels.length>0&&labels.every(isPercentile);if(labelsArePercentiles){return {labels,series:normalizedSeries};}const seriesArePercentiles=normalizedSeries.length>0&&normalizedSeries.every(s=>isPercentile(s.name));if(!seriesArePercentiles||labels.length===0){return {labels,series:normalizedSeries};}const percentileLabels=normalizedSeries.map(s=>s.name.toUpperCase());const reshaped=labels.map((label,labelIndex)=>{const values=normalizedSeries.map(s=>{const value=s.values[labelIndex];return Number.isFinite(value)?value:NaN;});return {name:compactName(label||('Series '+(labelIndex+1))),color:linePalette[labelIndex%linePalette.length],values};}).filter(s=>s.values.some(v=>Number.isFinite(v)));if(reshaped.length===0){return {labels:percentileLabels,series:[]};}return {labels:percentileLabels,series:reshaped};}
|
|
1501
|
-
function renderGroupedCorrelationCharts(){const canvases=[...document.querySelectorAll('.grouped-correlation-canvas')];if(canvases.length===0)return;canvases.forEach(canvas=>{const key=canvas.dataset.groupedKey||'';const chart=getGroupedCorrelationChart(key);drawLatencyLine(canvas,chart);});}
|
|
1502
|
-
function renderUngroupedCorrelationCharts(){const canvases=[...document.querySelectorAll('.ungrouped-correlation-canvas')];if(canvases.length===0)return;canvases.forEach(canvas=>{const key=canvas.dataset.ungroupedKey||'';const rawChart=getUngroupedCorrelationChart(key);const chart=normalizeUngroupedCorrelationChart(rawChart);drawLatencyLine(canvas,chart);});}
|
|
1503
|
-
function initPanePan(){if(!tabsPane)return;let activePointerId=null;let startY=0;let startScroll=0;tabsPane.addEventListener('pointerdown',event=>{if(event.button!==0)return;if(event.target&&event.target.closest&&event.target.closest('button,a,input,textarea,select,label'))return;activePointerId=event.pointerId;startY=event.clientY;startScroll=tabsPane.scrollTop;tabsPane.classList.add('panning');tabsPane.setPointerCapture(event.pointerId);});tabsPane.addEventListener('pointermove',event=>{if(activePointerId!==event.pointerId)return;const delta=event.clientY-startY;tabsPane.scrollTop=startScroll-delta;});const stopPan=event=>{if(activePointerId!==event.pointerId)return;activePointerId=null;tabsPane.classList.remove('panning');};tabsPane.addEventListener('pointerup',stopPan);tabsPane.addEventListener('pointercancel',stopPan);tabsPane.addEventListener('lostpointercapture',()=>{activePointerId=null;tabsPane.classList.remove('panning');});}
|
|
1504
|
-
function renderAllCharts(){renderCharts();renderUngroupedCorrelationCharts();renderGroupedCorrelationCharts();}
|
|
1505
|
-
const storedReportTheme=(()=>{try{return localStorage.getItem(reportThemeKey);}catch{return null;}})();
|
|
1506
|
-
applyReportTheme(storedReportTheme==='dark'?'dark':'light');
|
|
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();});}
|
|
1508
|
-
btns.forEach(b=>b.addEventListener('click',()=>{show(b.dataset.tab);requestAnimationFrame(renderAllCharts);}));
|
|
1509
|
-
if(btns.length>0){show(btns[0].dataset.tab);}renderAllCharts();initPanePan();window.addEventListener('resize',renderAllCharts);
|
|
1805
|
+
${reporting_svg_js_1.REPORT_SVG_SCRIPT.trimStart()}
|
|
1510
1806
|
</script>
|
|
1511
1807
|
</body>
|
|
1512
1808
|
</html>`;
|