@loadstrike/loadstrike-sdk 1.0.31001 → 1.0.32601
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 +16 -2
- package/dist/cjs/internal/prometheus-remote-write.js +37 -0
- package/dist/cjs/internal/reporting-sink-http-error.js +17 -0
- package/dist/cjs/internal/vendor-metric-payloads.js +390 -0
- package/dist/cjs/iteration-observations.js +24 -8
- package/dist/cjs/local-report-input.js +21 -0
- package/dist/cjs/local.js +48 -63
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-containment.js +242 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +413 -136
- package/dist/cjs/runtime.js +237 -8
- package/dist/cjs/sinks.js +1337 -38
- package/dist/cjs/transports.js +1339 -151
- package/dist/esm/internal/prometheus-remote-write.js +31 -0
- package/dist/esm/internal/reporting-sink-http-error.js +13 -0
- package/dist/esm/internal/vendor-metric-payloads.js +382 -0
- package/dist/esm/iteration-observations.js +24 -8
- package/dist/esm/local-report-input.js +17 -0
- package/dist/esm/local.js +49 -64
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-containment.js +238 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +413 -136
- package/dist/esm/runtime.js +239 -10
- package/dist/esm/sinks.js +1334 -35
- package/dist/esm/transports.js +1335 -151
- package/dist/types/contracts.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/internal/prometheus-remote-write.d.ts +2 -0
- package/dist/types/internal/reporting-sink-http-error.d.ts +6 -0
- package/dist/types/internal/vendor-metric-payloads.d.ts +48 -0
- package/dist/types/local-report-input.d.ts +6 -0
- package/dist/types/local.d.ts +0 -6
- package/dist/types/report-history.d.ts +124 -0
- package/dist/types/reporting-containment.d.ts +2 -0
- package/dist/types/reporting-svg.d.ts +2 -0
- package/dist/types/reporting.d.ts +6 -3
- package/dist/types/runtime.d.ts +24 -0
- package/dist/types/sinks.d.ts +134 -17
- package/dist/types/transports.d.ts +2 -0
- package/package.json +9 -3
- package/dist/cjs/internal-build.js +0 -4
- package/dist/esm/internal-build.js +0 -1
- package/dist/types/internal-build.d.ts +0 -1
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();
|
|
@@ -295,6 +298,40 @@ function loadStrikeNodeTypeTag(value) {
|
|
|
295
298
|
return asInt(value);
|
|
296
299
|
}
|
|
297
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
|
+
}
|
|
298
335
|
function parseUtcDate(value) {
|
|
299
336
|
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
300
337
|
return value;
|
|
@@ -424,6 +461,9 @@ function buildDotnetTableHtml(rows, wrapInCard = true) {
|
|
|
424
461
|
return parts.join("");
|
|
425
462
|
}
|
|
426
463
|
function formatReportTableHeader(header) {
|
|
464
|
+
if (header === "UnmatchedDestination") {
|
|
465
|
+
return "Unmatched Destination";
|
|
466
|
+
}
|
|
427
467
|
if (header === "LatencyStdDev") {
|
|
428
468
|
return "LatencyStdDev (ms)";
|
|
429
469
|
}
|
|
@@ -521,21 +561,14 @@ function buildFailedEventRows(plugins) {
|
|
|
521
561
|
}
|
|
522
562
|
function tryParseReportFloat(value) {
|
|
523
563
|
const parsed = Number.parseFloat(asString(value));
|
|
524
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
564
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
525
565
|
}
|
|
526
|
-
function
|
|
527
|
-
if (
|
|
566
|
+
function percentileFromOrdered(values, percentileValue) {
|
|
567
|
+
if (values.length === 0) {
|
|
528
568
|
return 0;
|
|
529
569
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
function percentileNumeric(values, percentileValue) {
|
|
533
|
-
if (!values.length) {
|
|
534
|
-
return 0;
|
|
535
|
-
}
|
|
536
|
-
const ordered = [...values].sort((left, right) => left - right);
|
|
537
|
-
const index = Math.min(Math.max(Math.ceil(percentileValue * ordered.length) - 1, 0), ordered.length - 1);
|
|
538
|
-
return ordered[index];
|
|
570
|
+
const index = Math.min(Math.max(Math.ceil(percentileValue * values.length) - 1, 0), values.length - 1);
|
|
571
|
+
return values[index];
|
|
539
572
|
}
|
|
540
573
|
function buildGroupedCorrelationChartPayloads(rows) {
|
|
541
574
|
const grouped = new Map();
|
|
@@ -553,33 +586,48 @@ function buildGroupedCorrelationChartPayloads(rows) {
|
|
|
553
586
|
}
|
|
554
587
|
return [...grouped.values()]
|
|
555
588
|
.filter((group) => group.rows.some((row) => tryParseReportFloat(row.LatencyP50Ms) !== undefined
|
|
589
|
+
|| tryParseReportFloat(row.LatencyP75Ms) !== undefined
|
|
556
590
|
|| tryParseReportFloat(row.LatencyP80Ms) !== undefined
|
|
557
591
|
|| tryParseReportFloat(row.LatencyP85Ms) !== undefined
|
|
558
592
|
|| tryParseReportFloat(row.LatencyP90Ms) !== undefined
|
|
559
593
|
|| tryParseReportFloat(row.LatencyP95Ms) !== undefined
|
|
560
|
-
|| tryParseReportFloat(row.LatencyP99Ms) !== undefined
|
|
594
|
+
|| tryParseReportFloat(row.LatencyP99Ms) !== undefined
|
|
595
|
+
|| tryParseReportFloat(row.LatencyMaxMs ?? row.LatencyMax) !== undefined))
|
|
561
596
|
.sort((left, right) => left.key[0] === right.key[0]
|
|
562
597
|
? left.key[1].localeCompare(right.key[1])
|
|
563
598
|
: left.key[0].localeCompare(right.key[0]))
|
|
564
599
|
.map((group, index) => ({
|
|
565
600
|
title: `${group.key[0]}: ${group.key[1]}`,
|
|
566
|
-
subtitle: group.rows.length > 1
|
|
601
|
+
subtitle: group.rows.length > 1
|
|
602
|
+
? `${group.rows.length} distinct scenario and destination rows.`
|
|
603
|
+
: "Single grouped row.",
|
|
567
604
|
chart: {
|
|
568
|
-
labels: ["P50", "P80", "P85", "P90", "P95", "P99"],
|
|
569
|
-
series: [
|
|
570
|
-
{
|
|
571
|
-
|
|
572
|
-
|
|
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],
|
|
573
619
|
values: [
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
|
580
628
|
]
|
|
581
|
-
}
|
|
582
|
-
|
|
629
|
+
};
|
|
630
|
+
})
|
|
583
631
|
}
|
|
584
632
|
}));
|
|
585
633
|
}
|
|
@@ -602,7 +650,6 @@ function buildUngroupedCorrelationChartPayload(rows) {
|
|
|
602
650
|
grouped.set(key, { scenario, destination, statusCode, latencies: [latency] });
|
|
603
651
|
}
|
|
604
652
|
}
|
|
605
|
-
const nameCount = new Map();
|
|
606
653
|
const series = [...grouped.values()]
|
|
607
654
|
.sort((left, right) => {
|
|
608
655
|
const leftKey = `${left.scenario}|${left.destination}|${left.statusCode}`;
|
|
@@ -610,25 +657,24 @@ function buildUngroupedCorrelationChartPayload(rows) {
|
|
|
610
657
|
return leftKey.localeCompare(rightKey);
|
|
611
658
|
})
|
|
612
659
|
.map((row, index) => {
|
|
613
|
-
const
|
|
614
|
-
const key = baseName.toLowerCase();
|
|
615
|
-
const count = nameCount.get(key) ?? 0;
|
|
616
|
-
nameCount.set(key, count + 1);
|
|
660
|
+
const orderedLatencies = [...row.latencies].sort((left, right) => left - right);
|
|
617
661
|
return {
|
|
618
|
-
name:
|
|
662
|
+
name: `${row.scenario} | ${row.destination} | status ${row.statusCode}`,
|
|
619
663
|
color: ["#38bdf8", "#22c55e", "#f59e0b", "#a855f7", "#f43f5e", "#06b6d4", "#14b8a6", "#eab308", "#818cf8", "#84cc16"][index % 10],
|
|
620
664
|
values: [
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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]
|
|
627
673
|
]
|
|
628
674
|
};
|
|
629
675
|
});
|
|
630
676
|
return {
|
|
631
|
-
labels: ["P50", "P80", "P85", "P90", "P95", "P99"],
|
|
677
|
+
labels: ["P50", "P75", "P80", "P85", "P90", "P95", "P99", "Max"],
|
|
632
678
|
series
|
|
633
679
|
};
|
|
634
680
|
}
|
|
@@ -764,8 +810,34 @@ function hasMeasurementData(measurement) {
|
|
|
764
810
|
asString(reportValue(code, "statusCode", "StatusCode")).trim().length > 0 ||
|
|
765
811
|
asString(reportValue(code, "message", "Message")).trim().length > 0);
|
|
766
812
|
}
|
|
767
|
-
function appendChartCard(parts, id, title) {
|
|
768
|
-
|
|
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>");
|
|
769
841
|
}
|
|
770
842
|
function hasChartPointData(points) {
|
|
771
843
|
return Array.isArray(points) && points.length > 0;
|
|
@@ -776,7 +848,7 @@ function hasPieChartData(points) {
|
|
|
776
848
|
function hasLatencyTrendData(chart) {
|
|
777
849
|
const labels = reportArray(chart, "labels", "Labels");
|
|
778
850
|
const series = reportArray(chart, "series", "Series");
|
|
779
|
-
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));
|
|
780
852
|
}
|
|
781
853
|
function hasNonEmptyHints(plugin) {
|
|
782
854
|
return reportArray(plugin, "hints", "Hints").some((hint) => asString(hint).trim().length > 0);
|
|
@@ -919,11 +991,169 @@ function buildDotnetStatusCodeClassChart(scenarios) {
|
|
|
919
991
|
{ label: "Other", value: buckets.Other, color: "#8b5cf6" }
|
|
920
992
|
].filter((entry) => entry.value > 0);
|
|
921
993
|
}
|
|
922
|
-
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) {
|
|
923
1150
|
const scenarios = reportScenarios(nodeStats);
|
|
924
|
-
const
|
|
925
|
-
.map((scenario) => ({ scenario, measurement:
|
|
1151
|
+
const genuineCombinedScenarios = scenarios
|
|
1152
|
+
.map((scenario) => ({ scenario, measurement: genuineCombinedMeasurement(scenario) }))
|
|
926
1153
|
.filter((item) => item.measurement !== undefined);
|
|
1154
|
+
const history = localReportInput.history.status === "available"
|
|
1155
|
+
? buildHistoryChartData(localReportInput.history.points)
|
|
1156
|
+
: buildHistoryChartData([]);
|
|
927
1157
|
return {
|
|
928
1158
|
overallOutcome: [
|
|
929
1159
|
{ label: "OK", value: reportTotalOkCount(nodeStats, scenarios), color: "#18a957" },
|
|
@@ -934,11 +1164,15 @@ function buildDotnetChartData(nodeStats) {
|
|
|
934
1164
|
value: reportRequestCountValue(scenario),
|
|
935
1165
|
color: "#3b82f6"
|
|
936
1166
|
})),
|
|
937
|
-
scenarioP95Latency:
|
|
1167
|
+
scenarioP95Latency: genuineCombinedScenarios.map(({ scenario, measurement }) => ({
|
|
938
1168
|
label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
|
|
939
1169
|
value: asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95")),
|
|
940
1170
|
color: "#8b5cf6"
|
|
941
1171
|
})),
|
|
1172
|
+
scenarioP95LatencyByOutcome: {
|
|
1173
|
+
labels: scenarios.map((scenario) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
|
|
1174
|
+
series: buildOutcomeLatencySeries(scenarios, "P95", "percent95", "Percent95")
|
|
1175
|
+
},
|
|
942
1176
|
scenarioRps: scenarios.map((scenario) => ({
|
|
943
1177
|
label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
|
|
944
1178
|
value: reportDurationSeconds(reportDurationValue(scenario)) <= 0
|
|
@@ -960,17 +1194,18 @@ function buildDotnetChartData(nodeStats) {
|
|
|
960
1194
|
})),
|
|
961
1195
|
statusCodeClasses: buildDotnetStatusCodeClassChart(scenarios),
|
|
962
1196
|
scenarioLatencyTrend: {
|
|
963
|
-
labels:
|
|
1197
|
+
labels: scenarios.map((scenario) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
|
|
964
1198
|
series: [
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1199
|
+
...buildOutcomeLatencySeries(scenarios, "P50", "percent50", "Percent50"),
|
|
1200
|
+
...buildOutcomeLatencySeries(scenarios, "P75", "percent75", "Percent75"),
|
|
1201
|
+
...buildOutcomeLatencySeries(scenarios, "P95", "percent95", "Percent95"),
|
|
1202
|
+
...buildOutcomeLatencySeries(scenarios, "P99", "percent99", "Percent99")
|
|
969
1203
|
]
|
|
970
|
-
}
|
|
1204
|
+
},
|
|
1205
|
+
...history
|
|
971
1206
|
};
|
|
972
1207
|
}
|
|
973
|
-
function buildDotnetSummaryHtml(nodeStats) {
|
|
1208
|
+
function buildDotnetSummaryHtml(nodeStats, localReportInput) {
|
|
974
1209
|
const scenarios = reportScenarios(nodeStats);
|
|
975
1210
|
const allRequests = reportTotalRequestCount(nodeStats, scenarios);
|
|
976
1211
|
const allOk = reportTotalOkCount(nodeStats, scenarios);
|
|
@@ -992,11 +1227,19 @@ function buildDotnetSummaryHtml(nodeStats) {
|
|
|
992
1227
|
? scenario
|
|
993
1228
|
: winner;
|
|
994
1229
|
}, undefined);
|
|
995
|
-
const latencyRows =
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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);
|
|
1000
1243
|
const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
|
|
1001
1244
|
const nodeInfo = reportObject(nodeStats, "nodeInfo", "NodeInfo");
|
|
1002
1245
|
const totalBytes = reportTotalBytes(nodeStats, scenarios);
|
|
@@ -1009,39 +1252,62 @@ function buildDotnetSummaryHtml(nodeStats) {
|
|
|
1009
1252
|
appendReportLine(parts, `<div class="stat-card"><div class="stat-label">Duration</div><div class="stat-value">${escapeHtml(formatDotnetTimeSpan(reportDurationValue(nodeStats)))}</div></div>`);
|
|
1010
1253
|
appendReportLine(parts, `<div class="stat-card"><div class="stat-label">Total Bytes</div><div class="stat-value">${totalBytes}</div></div>`);
|
|
1011
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>`);
|
|
1012
|
-
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>`);
|
|
1013
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
|
+
}
|
|
1014
1266
|
const charts = [];
|
|
1015
1267
|
if (hasPieChartData(chartData.overallOutcome)) {
|
|
1016
|
-
appendChartCard(charts, "chart-outcome", "Success vs Fail");
|
|
1268
|
+
appendChartCard(charts, "chart-outcome", "Success vs Fail", "overallOutcome", "donut", "requests", "Outcome");
|
|
1017
1269
|
}
|
|
1018
1270
|
if (hasChartPointData(chartData.scenarioRequests)) {
|
|
1019
|
-
appendChartCard(charts, "chart-scenario-requests", "Requests by Scenario");
|
|
1271
|
+
appendChartCard(charts, "chart-scenario-requests", "Requests by Scenario", "scenarioRequests", "bar", "requests", "Requests");
|
|
1020
1272
|
}
|
|
1021
|
-
if (
|
|
1022
|
-
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");
|
|
1023
1275
|
}
|
|
1024
1276
|
if (hasChartPointData(chartData.scenarioRps)) {
|
|
1025
|
-
appendChartCard(charts, "chart-scenario-rps", "RPS by Scenario");
|
|
1277
|
+
appendChartCard(charts, "chart-scenario-rps", "RPS by Scenario", "scenarioRps", "bar", "req/s", "Rate");
|
|
1026
1278
|
}
|
|
1027
1279
|
if (hasChartPointData(chartData.scenarioFailRate)) {
|
|
1028
|
-
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");
|
|
1029
1281
|
}
|
|
1030
1282
|
if (hasChartPointData(chartData.scenarioBytes)) {
|
|
1031
|
-
appendChartCard(charts, "chart-scenario-bytes", "Bytes by Scenario");
|
|
1283
|
+
appendChartCard(charts, "chart-scenario-bytes", "Bytes by Scenario", "scenarioBytes", "bar", "bytes", "Transferred");
|
|
1032
1284
|
}
|
|
1033
1285
|
if (hasPieChartData(chartData.statusCodeClasses)) {
|
|
1034
|
-
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");
|
|
1035
1287
|
}
|
|
1036
1288
|
if (hasLatencyTrendData(chartData.scenarioLatencyTrend)) {
|
|
1037
|
-
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}"`);
|
|
1038
1302
|
}
|
|
1039
1303
|
if (charts.length > 0) {
|
|
1040
|
-
appendReportLine(parts, "<div class=\"card\">");
|
|
1304
|
+
appendReportLine(parts, "<div class=\"card chart-collection\" data-chart-collection data-grid-size=\"comfortable\">");
|
|
1041
1305
|
appendReportLine(parts, "<h2>Charts</h2>");
|
|
1306
|
+
appendChartCollectionTools(parts);
|
|
1042
1307
|
appendReportLine(parts, "<div class=\"chart-grid\">");
|
|
1043
1308
|
parts.push(...charts);
|
|
1044
1309
|
appendReportLine(parts, "</div>");
|
|
1310
|
+
appendReportLine(parts, "<div class=\"chart-empty\" data-chart-empty>No charts match this search.</div>");
|
|
1045
1311
|
appendReportLine(parts, "</div>");
|
|
1046
1312
|
}
|
|
1047
1313
|
if (latencyRows.length > 0) {
|
|
@@ -1058,7 +1324,7 @@ function buildDotnetSummaryHtml(nodeStats) {
|
|
|
1058
1324
|
appendReportLine(parts, `<tr><th>Created (UTC)</th><td>${escapeHtml(formatDotnetDateTime(reportValue(testInfo, "createdUtc", "CreatedUtc", "created", "Created")))}</td></tr>`);
|
|
1059
1325
|
appendReportLine(parts, `<tr><th>Machine</th><td>${escapeHtml(reportValue(nodeInfo, "machineName", "MachineName"))}</td></tr>`);
|
|
1060
1326
|
appendReportLine(parts, `<tr><th>OS</th><td>${escapeHtml(reportValue(nodeInfo, "os", "OS"))}</td></tr>`);
|
|
1061
|
-
appendReportLine(parts, `<tr><th>
|
|
1327
|
+
appendReportLine(parts, `<tr><th>Runtime</th><td>${escapeHtml(loadStrikeRuntimeVersionLabel(nodeInfo))}</td></tr>`);
|
|
1062
1328
|
appendReportLine(parts, `<tr><th>Processor</th><td>${escapeHtml(reportValue(nodeInfo, "processor", "Processor"))}</td></tr>`);
|
|
1063
1329
|
appendReportLine(parts, `<tr><th>Cores</th><td>${asInt(reportValue(nodeInfo, "coresCount", "CoresCount"))}</td></tr>`);
|
|
1064
1330
|
appendReportLine(parts, `<tr><th>Operation</th><td>${escapeHtml(reportValue(nodeInfo, "currentOperation", "CurrentOperation"))}</td></tr>`);
|
|
@@ -1110,8 +1376,30 @@ function buildDotnetThresholdHtml(nodeStats) {
|
|
|
1110
1376
|
function buildDotnetMetricHtml(nodeStats) {
|
|
1111
1377
|
return buildDotnetTableHtml(buildDotnetMetricRows(nodeStats));
|
|
1112
1378
|
}
|
|
1113
|
-
function
|
|
1114
|
-
|
|
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
|
+
];
|
|
1115
1403
|
const stats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
|
|
1116
1404
|
const topLevelSegments = reportArray(nodeStats, "schedulerSegments", "SchedulerSegments");
|
|
1117
1405
|
const segments = topLevelSegments.length > 0
|
|
@@ -1126,6 +1414,9 @@ function buildDotnetGeneratorDeliveryHtml(nodeStats) {
|
|
|
1126
1414
|
appendReportLine(parts, "<div class=\"card\">");
|
|
1127
1415
|
appendReportLine(parts, "<h2>Generator Delivery</h2>");
|
|
1128
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
|
+
}
|
|
1129
1420
|
appendReportLine(parts, "<div class=\"card-grid\">");
|
|
1130
1421
|
for (const [label, value] of [
|
|
1131
1422
|
["Configured Max In Flight", asInt(reportValue(stats, "configuredMaxInFlight", "ConfiguredMaxInFlight"))],
|
|
@@ -1142,7 +1433,7 @@ function buildDotnetGeneratorDeliveryHtml(nodeStats) {
|
|
|
1142
1433
|
}
|
|
1143
1434
|
appendReportLine(parts, "</div></div>");
|
|
1144
1435
|
if (warnings.length) {
|
|
1145
|
-
appendReportLine(parts, "<div class=\"card\"><h2>Generator Warnings</h2>");
|
|
1436
|
+
appendReportLine(parts, "<div class=\"card\"><h2>Generator and Reporting Warnings</h2>");
|
|
1146
1437
|
parts.push(buildDotnetTableHtml(warnings, false));
|
|
1147
1438
|
appendReportLine(parts, "</div>");
|
|
1148
1439
|
}
|
|
@@ -1167,7 +1458,7 @@ function buildDotnetGeneratorDeliveryHtml(nodeStats) {
|
|
|
1167
1458
|
}
|
|
1168
1459
|
return parts.join("");
|
|
1169
1460
|
}
|
|
1170
|
-
function hasDotnetGeneratorDeliveryData(nodeStats) {
|
|
1461
|
+
function hasDotnetGeneratorDeliveryData(nodeStats, localReportInput = (0, local_report_input_js_1.emptyLocalReportInput)()) {
|
|
1171
1462
|
const schedulerStats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
|
|
1172
1463
|
const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
|
|
1173
1464
|
const hasNonZeroDecimal = (value) => {
|
|
@@ -1175,6 +1466,7 @@ function hasDotnetGeneratorDeliveryData(nodeStats) {
|
|
|
1175
1466
|
return text.trim().length > 0 && text !== "0";
|
|
1176
1467
|
};
|
|
1177
1468
|
return reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings").length > 0
|
|
1469
|
+
|| localReportInput.history.status === "unavailable"
|
|
1178
1470
|
|| reportArray(nodeStats, "schedulerSegments", "SchedulerSegments").length > 0
|
|
1179
1471
|
|| reportArray(schedulerStats, "segments", "Segments").length > 0
|
|
1180
1472
|
|| asInt(reportValue(schedulerStats, "configuredMaxInFlight", "ConfiguredMaxInFlight")) > 0
|
|
@@ -1194,15 +1486,19 @@ function buildDotnetGroupedCorrelationSummaryHtml(rows, groupedChartKey) {
|
|
|
1194
1486
|
return parts.join("");
|
|
1195
1487
|
}
|
|
1196
1488
|
appendReportLine(parts);
|
|
1197
|
-
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);
|
|
1198
1492
|
appendReportLine(parts, "<div class=\"chart-grid correlation-chart-grid\">");
|
|
1199
1493
|
payloads.forEach((payload, index) => {
|
|
1200
1494
|
const chartKey = `${groupedChartKey}-value-${index}`;
|
|
1201
1495
|
const chartJson = escapeJsonForHtmlScript(JSON.stringify(payload.chart));
|
|
1202
1496
|
appendReportLine(parts, `<script type="application/json" data-grouped-correlation-chart="${escapeHtml(chartKey)}">${chartJson}</script>`);
|
|
1203
|
-
|
|
1497
|
+
appendChartCard(parts, `${chartKey}-chart`, payload.title, "grouped-correlation", "line", "ms", "Latency", "grouped-correlation-canvas", `data-grouped-key="${escapeHtml(chartKey)}"`);
|
|
1204
1498
|
});
|
|
1205
1499
|
appendReportLine(parts, "</div>");
|
|
1500
|
+
appendReportLine(parts, "<div class=\"chart-empty\" data-chart-empty>No charts match this search.</div>");
|
|
1501
|
+
appendReportLine(parts, "</div>");
|
|
1206
1502
|
return parts.join("");
|
|
1207
1503
|
}
|
|
1208
1504
|
function buildDotnetUngroupedCorrelationSummaryHtml(rows, groupedChartKey) {
|
|
@@ -1214,8 +1510,12 @@ function buildDotnetUngroupedCorrelationSummaryHtml(rows, groupedChartKey) {
|
|
|
1214
1510
|
appendReportLine(parts);
|
|
1215
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>");
|
|
1216
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);
|
|
1217
1515
|
appendReportLine(parts, "<div class=\"chart-grid correlation-chart-grid\">");
|
|
1218
|
-
|
|
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>");
|
|
1219
1519
|
appendReportLine(parts, "</div>");
|
|
1220
1520
|
}
|
|
1221
1521
|
return parts.join("");
|
|
@@ -1227,8 +1527,8 @@ function buildDotnetPluginHints(plugin) {
|
|
|
1227
1527
|
const hints = reportArray(plugin, "hints", "Hints").map((hint) => asString(hint).trim()).filter((hint) => hint.length > 0);
|
|
1228
1528
|
return `<div class="card"><strong>Hints</strong><ul>${hints.map((hint) => `<li>${escapeHtml(hint)}</li>`).join("")}</ul></div>`;
|
|
1229
1529
|
}
|
|
1230
|
-
function buildDotnetHtmlTabs(nodeStats) {
|
|
1231
|
-
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)]];
|
|
1232
1532
|
const scenarioRows = buildDotnetScenarioRows(nodeStats);
|
|
1233
1533
|
if (scenarioRows.length) {
|
|
1234
1534
|
tabs.push(["scenarios", "Scenarios", buildDotnetTableHtml(scenarioRows)]);
|
|
@@ -1262,8 +1562,8 @@ function buildDotnetHtmlTabs(nodeStats) {
|
|
|
1262
1562
|
if (metricRows.length) {
|
|
1263
1563
|
tabs.push(["metrics", "Metrics", buildDotnetTableHtml(metricRows)]);
|
|
1264
1564
|
}
|
|
1265
|
-
if (hasDotnetGeneratorDeliveryData(nodeStats)) {
|
|
1266
|
-
tabs.push(["generator-delivery", "Generator Delivery", buildDotnetGeneratorDeliveryHtml(nodeStats)]);
|
|
1565
|
+
if (hasDotnetGeneratorDeliveryData(nodeStats, localReportInput)) {
|
|
1566
|
+
tabs.push(["generator-delivery", "Generator Delivery", buildDotnetGeneratorDeliveryHtml(nodeStats, localReportInput)]);
|
|
1267
1567
|
}
|
|
1268
1568
|
for (const plugin of reportArray(nodeStats, "pluginsData", "PluginsData")) {
|
|
1269
1569
|
const pluginName = asString(reportValue(plugin, "pluginName", "PluginName"));
|
|
@@ -1410,41 +1710,47 @@ function buildDotnetMarkdownReport(nodeStats) {
|
|
|
1410
1710
|
return reportLines(lines);
|
|
1411
1711
|
}
|
|
1412
1712
|
/**
|
|
1413
|
-
*
|
|
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.
|
|
1414
1716
|
*/
|
|
1415
|
-
function buildDotnetHtmlReport(nodeStats) {
|
|
1416
|
-
const tabs = buildDotnetHtmlTabs(nodeStats);
|
|
1417
|
-
const buttonsHtml = tabs
|
|
1418
|
-
|
|
1419
|
-
|
|
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)));
|
|
1420
1726
|
const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
|
|
1421
1727
|
const template = `<!doctype html>
|
|
1422
1728
|
<html lang="en">
|
|
1423
1729
|
<head>
|
|
1424
1730
|
<meta charset="utf-8" />
|
|
1425
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'" />
|
|
1426
1733
|
<title>LoadStrike Report</title>
|
|
1427
1734
|
<style>
|
|
1428
|
-
: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;--
|
|
1429
|
-
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)}
|
|
1430
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}
|
|
1431
1738
|
.wrap{max-width:1440px;margin:0 auto;padding:20px}
|
|
1432
|
-
.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}
|
|
1433
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}
|
|
1434
|
-
.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}
|
|
1435
1742
|
body[data-theme='dark'] .report-logo{--report-logo-scale:1.175}
|
|
1436
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}
|
|
1437
1744
|
.theme-toggle-report:hover{border-color:var(--accent);transform:translateY(-1px)}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
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)}
|
|
1442
1748
|
.meta{display:flex;gap:14px;flex-wrap:wrap;color:var(--muted);font-size:13px;margin-bottom:16px}
|
|
1443
1749
|
.meta span{background:var(--chip);border:1px solid var(--line);padding:6px 10px;border-radius:999px}
|
|
1444
1750
|
.report-layout{display:grid;grid-template-columns:280px minmax(0,1fr);gap:14px;align-items:start}
|
|
1445
1751
|
.tabs-pane{position:sticky;top:12px;max-height:calc(100vh - 24px);overflow:auto;overscroll-behavior:contain;padding-right:4px;cursor:grab}
|
|
1446
1752
|
.tabs-pane.panning{cursor:grabbing;user-select:none}
|
|
1447
|
-
.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}
|
|
1448
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%}
|
|
1449
1755
|
.tab-btn:hover{border-color:var(--accent);background:var(--chip)}
|
|
1450
1756
|
.tab-btn.active{background:linear-gradient(180deg,#2f66db 0,#2754b8 100%);border-color:#3f73e0}
|
|
@@ -1463,22 +1769,19 @@ th{background:var(--panelAlt);position:sticky;top:0;z-index:1}
|
|
|
1463
1769
|
.value-fail{color:var(--fail)}
|
|
1464
1770
|
.chart-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:12px}
|
|
1465
1771
|
.chart-card{padding:12px;border:1px solid var(--line);border-radius:10px;background:var(--chartPanel)}
|
|
1466
|
-
.chart-
|
|
1467
|
-
.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}
|
|
1468
1773
|
.correlation-chart-card{max-width:720px;width:100%}
|
|
1469
|
-
.correlation-chart-card .chart-canvas{height:320px}
|
|
1470
1774
|
.chart-card h3,.chart-card p{color:var(--chartLegendText)}
|
|
1471
1775
|
.table-wrap{overflow:auto;max-height:70vh}
|
|
1472
|
-
|
|
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}}
|
|
1473
1778
|
</style>
|
|
1474
1779
|
</head>
|
|
1475
1780
|
<body data-theme="light">
|
|
1476
1781
|
<div class="wrap">
|
|
1477
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>
|
|
1478
1783
|
<div class="report-brand">
|
|
1479
|
-
<div class="report-logo-slot">
|
|
1480
|
-
<img src="__LOGO_LIGHT__" alt="LoadStrike logo" class="report-logo" data-report-logo data-logo-light="__LOGO_LIGHT__" data-logo-dark="__LOGO_DARK__" />
|
|
1481
|
-
</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>
|
|
1482
1785
|
<h1>LoadStrike Report</h1>
|
|
1483
1786
|
</div>
|
|
1484
1787
|
<div class="meta">
|
|
@@ -1488,44 +1791,18 @@ th{background:var(--panelAlt);position:sticky;top:0;z-index:1}
|
|
|
1488
1791
|
<span>Duration: <strong>__DURATION__</strong></span>
|
|
1489
1792
|
</div>
|
|
1490
1793
|
<div class="report-layout">
|
|
1491
|
-
<aside class="tabs-pane" id="tab-pane">
|
|
1492
|
-
|
|
1493
|
-
__BUTTONS__</div>
|
|
1494
|
-
</aside>
|
|
1794
|
+
<aside class="tabs-pane" id="tab-pane"><div class="tabs">
|
|
1795
|
+
__BUTTONS__</div></aside>
|
|
1495
1796
|
<div class="tabs-content">
|
|
1496
1797
|
__SECTIONS__</div>
|
|
1497
1798
|
</div>
|
|
1498
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>
|
|
1499
1803
|
<script>
|
|
1500
1804
|
const reportCharts=__CHART_DATA__;
|
|
1501
|
-
|
|
1502
|
-
const tabSections=[...document.querySelectorAll('.tab')];
|
|
1503
|
-
const tabsPane=document.getElementById('tab-pane');
|
|
1504
|
-
const linePalette=['#38bdf8','#22c55e','#f59e0b','#a855f7','#f43f5e','#14b8a6','#eab308','#818cf8','#06b6d4','#84cc16'];
|
|
1505
|
-
const reportThemeKey='loadstrike-report-theme';
|
|
1506
|
-
const reportThemeToggle=document.querySelector('[data-report-theme-toggle]');
|
|
1507
|
-
const reportLogo=document.querySelector('[data-report-logo]');
|
|
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');}}
|
|
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));}
|
|
1510
|
-
function formatMetric(v){if(!Number.isFinite(v))return '0';return Math.abs(v)>=100?v.toFixed(0):v.toFixed(2);}
|
|
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};}
|
|
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);}
|
|
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);}}
|
|
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;}}
|
|
1515
|
-
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;}}}
|
|
1516
|
-
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);}
|
|
1517
|
-
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:[]};}}
|
|
1518
|
-
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:[]};}}
|
|
1519
|
-
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};}
|
|
1520
|
-
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);});}
|
|
1521
|
-
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);});}
|
|
1522
|
-
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');});}
|
|
1523
|
-
function renderAllCharts(){renderCharts();renderUngroupedCorrelationCharts();renderGroupedCorrelationCharts();}
|
|
1524
|
-
const storedReportTheme=(()=>{try{return localStorage.getItem(reportThemeKey);}catch{return null;}})();
|
|
1525
|
-
applyReportTheme(storedReportTheme==='dark'?'dark':'light');
|
|
1526
|
-
if(reportThemeToggle){reportThemeToggle.addEventListener('click',()=>{const next=document.body.getAttribute('data-theme')==='dark'?'light':'dark';applyReportTheme(next);try{localStorage.setItem(reportThemeKey,next);}catch{}renderAllCharts();});}
|
|
1527
|
-
btns.forEach(b=>b.addEventListener('click',()=>{show(b.dataset.tab);requestAnimationFrame(renderAllCharts);}));
|
|
1528
|
-
if(btns.length>0){show(btns[0].dataset.tab);}renderAllCharts();initPanePan();window.addEventListener('resize',renderAllCharts);
|
|
1805
|
+
${reporting_svg_js_1.REPORT_SVG_SCRIPT.trimStart()}
|
|
1529
1806
|
</script>
|
|
1530
1807
|
</body>
|
|
1531
1808
|
</html>`;
|