review plot rendering and doublemetric opts

This commit is contained in:
Christoph Kluge
2026-01-21 12:21:52 +01:00
parent f416be77f7
commit f18ae35030
8 changed files with 205 additions and 546 deletions

View File

@@ -6,9 +6,6 @@
--> -->
<script> <script>
// import {
// getContext
// } from "svelte"
import { import {
queryStore, queryStore,
gql, gql,
@@ -55,9 +52,6 @@
let to = $state(new Date(Date.now())); let to = $state(new Date(Date.now()));
let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400); let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
let colWidthStates = $state(0); let colWidthStates = $state(0);
let colWidthRoof = $state(0);
let colWidthTotals = $state(0);
let colWidthStacked = $state(0);
/* Derived */ /* Derived */
// States for Stacked charts // States for Stacked charts
@@ -522,29 +516,38 @@
</Card> </Card>
</Col> </Col>
<Col> <!-- Total Cluster Metric in Time SUMS--> <!-- Total Cluster Metric in Time SUMS-->
<div bind:clientWidth={colWidthTotals}> <Col class="text-center">
<DoubleMetric <h5 class="mt-2 mb-0">
width={colWidthTotals} Cluster Utilization (
timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60} <span style="color: #0000ff;">
numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0} {`${$statusQuery?.data?.clusterMetrics?.metrics[0]?.name} (${$statusQuery?.data?.clusterMetrics?.metrics[0]?.unit?.prefix}${$statusQuery?.data?.clusterMetrics?.metrics[0]?.unit?.base})`}
metricData={$statusQuery?.data?.clusterMetrics?.metrics || []} </span>,
cluster={presetCluster} <span style="color: #ff0000;">
fixLinewidth={2} {`${$statusQuery?.data?.clusterMetrics?.metrics[1]?.name} (${$statusQuery?.data?.clusterMetrics?.metrics[1]?.unit?.prefix}${$statusQuery?.data?.clusterMetrics?.metrics[1]?.unit?.base})`}
/> </span>
)
</h5>
<div>
{#key $statusQuery?.data?.clusterMetrics}
<DoubleMetric
timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60}
numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0}
metricData={$statusQuery?.data?.clusterMetrics?.metrics || []}
publicMode
/>
{/key}
</div> </div>
</Col> </Col>
<Col> <!-- Nodes Roofline --> <Col> <!-- Nodes Roofline -->
<div bind:clientWidth={colWidthRoof}> <div>
{#key $statusQuery?.data?.nodeMetrics} {#key $statusQuery?.data?.nodeMetrics}
<Roofline <Roofline
colorBackground colorBackground
useColors={false} useColors={false}
useLegend={false} useLegend={false}
allowSizeChange allowSizeChange
width={colWidthRoof}
height={300}
cluster={presetCluster} cluster={presetCluster}
subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null} subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null}
roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics)} roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics)}
@@ -607,11 +610,10 @@
</Col> </Col>
<Col> <!-- Stacked SchedState --> <Col> <!-- Stacked SchedState -->
<div bind:clientWidth={colWidthStacked}> <div>
{#key $statesTimed?.data?.nodeStatesTimed} {#key $statesTimed?.data?.nodeStatesTimed}
<Stacked <Stacked
data={$statesTimed?.data?.nodeStatesTimed} data={$statesTimed?.data?.nodeStatesTimed}
width={colWidthStacked}
height={260} height={260}
ylabel="Nodes" ylabel="Nodes"
yunit = "#Count" yunit = "#Count"

View File

@@ -229,7 +229,7 @@
</DropdownToggle> </DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end"> <DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks <NavbarLinks
{clustersNames} {clusterNames}
{subclusterMap} {subclusterMap}
direction="right" direction="right"
links={views.filter( links={views.filter(

View File

@@ -4,123 +4,56 @@
Only width/height should change reactively. Only width/height should change reactively.
Properties: Properties:
- `metric String`: The metric name - `metricData [Data]`: Two series of metric data including unit info
- `scope String?`: Scope of the displayed data [Default: node] - `timestep Number`: Data timestep
- `numNodes Number`: Number of nodes from which metric data is aggregated
- `cluster String`: Cluster name of the parent job / data [Default: ""]
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: true]
- `enableFlip Bool?`: Whether to use legend tooltip flipping based on canvas size [Default: false]
- `publicMode Bool?`: Disables tooltip legend and enables larger colored axis labels [Default: false]
- `height Number?`: The plot height [Default: 300] - `height Number?`: The plot height [Default: 300]
- `timestep Number`: The timestep used for X-axis rendering
- `series [GraphQL.Series]`: The metric data object
- `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null]
- `cluster String?`: Cluster name of the parent job / data [Default: ""]
- `subCluster String`: Name of the subCluster of the parent job
- `isShared Bool?`: If this job used shared resources; for additional legend display [Default: false]
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false]
- `numhwthreads Number?`: Number of job HWThreads [Default: 0]
- `numaccs Number?`: Number of job Accelerators [Default: 0]
- `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null]
- `thersholdState Object?`: The last threshold state to preserve on user zoom [Default: null]
- `extendedLegendData Object?`: Additional information to be rendered in an extended legend [Default: null]
- `onZoom Func`: Callback function to handle zoom-in event
--> -->
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
import { formatNumber, formatDurationTime } from "../units.js"; import { formatNumber, formatDurationTime } from "../units.js";
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap"; import { Card } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
// metric, metricData,
width = 0,
height = 300,
fixLinewidth = null,
timestep, timestep,
numNodes, numNodes,
metricData, cluster,
// useStatsSeries = false,
// statisticsSeries = null,
cluster = "",
forNode = true, forNode = true,
// zoomState = null,
// thresholdState = null,
enableFlip = false, enableFlip = false,
publicMode = false, publicMode = false,
// onZoom height = 300,
} = $props(); } = $props();
/* Const Init */ /* Const Init */
const clusterCockpitConfig = getContext("cc-config"); const clusterCockpitConfig = getContext("cc-config");
const fixedLineColors = ["#0000ff", "#ff0000"]; // Plot only uses 2 Datasets: High Contrast const fixedLineColors = ["#0000ff", "#ff0000"]; // Plot only uses 2 Datasets: High Contrast
// const resampleConfig = getContext("resampling");
// const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
// const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
// const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
const renderSleepTime = 200; const renderSleepTime = 200;
// const backgroundColors = {
// normal: "rgba(255, 255, 255, 1.0)",
// caution: cbmode ? "rgba(239, 230, 69, 0.3)" : "rgba(255, 128, 0, 0.3)",
// alert: cbmode ? "rgba(225, 86, 44, 0.3)" : "rgba(255, 0, 0, 0.3)",
// };
/* Var Init */ /* Var Init */
let timeoutId = null; let timeoutId = null;
/* State Init */ /* State Init */
let plotWrapper = $state(null); let plotWrapper = $state(null);
let width = $state(0); // Wrapper Width
let uplot = $state(null); let uplot = $state(null);
/* Derived */ /* Derived */
const lineWidth = $derived(fixLinewidth ? fixLinewidth : clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio);
// const usesMeanStatsSeries = $derived((statisticsSeries?.mean && statisticsSeries.mean.length != 0));
// const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : null);
// const resampleResolutions = $derived(resampleConfig?.resolutions ? [...resampleConfig.resolutions] : null);
// const resampleMinimum = $derived(resampleConfig?.resolutions ? Math.min(...resampleConfig.resolutions) : null);
// const thresholds = $derived(findJobAggregationThresholds(
// subClusterTopology,
// metricConfig,
// scope,
// numhwthreads,
// numaccs
// ));
const longestSeries = $derived.by(() => {
// if (useStatsSeries) {
// return usesMeanStatsSeries ? statisticsSeries?.mean?.length : statisticsSeries?.median?.length;
// } else {
return metricData.reduce((n, m) => Math.max(n, m.data.length), 0);
// }
});
const maxX = $derived(longestSeries * timestep); const maxX = $derived(longestSeries * timestep);
// const maxY = $derived.by(() => { const lineWidth = $derived(publicMode ? 2 : clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio);
// let pendingY = 0; const longestSeries = $derived.by(() => {
// // if (useStatsSeries) { return metricData.reduce((n, m) => Math.max(n, m.data.length), 0);
// // pendingY = statisticsSeries.max.reduce( });
// // (max, x) => Math.max(max, x),
// // thresholds?.normal,
// // ) || thresholds?.normal
// // } else {
// pendingY = series.reduce(
// (max, series) => Math.max(max, series?.statistics?.max),
// thresholds?.normal,
// ) || thresholds?.normal;
// // }
// if (pendingY >= 10 * thresholds.peak) { // Derive Plot Params
// // Hard y-range render limit if outliers in series data let plotData = $derived.by(() => {
// return (10 * thresholds.peak);
// } else {
// return pendingY;
// }
// });
// const plotBands = $derived.by(() => {
// if (useStatsSeries) {
// return [
// { series: [2, 3], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
// { series: [3, 1], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
// ];
// };
// return null;
// })
const plotData = $derived.by(() => {
let pendingData = [new Array(longestSeries)]; let pendingData = [new Array(longestSeries)];
// X // X
if (forNode === true) { if (forNode === true) {
@@ -135,25 +68,15 @@
}; };
}; };
// Y // Y
// if (useStatsSeries) { for (let i = 0; i < metricData.length; i++) {
// pendingData.push(statisticsSeries.min); pendingData.push(metricData[i]?.data);
// pendingData.push(statisticsSeries.max); };
// if (usesMeanStatsSeries) {
// pendingData.push(statisticsSeries.mean);
// } else {
// pendingData.push(statisticsSeries.median);
// }
// } else {
for (let i = 0; i < metricData.length; i++) {
pendingData.push(metricData[i]?.data);
};
// };
return pendingData; return pendingData;
}) })
const plotSeries = $derived.by(() => {
let plotSeries = $derived.by(() => {
// X
let pendingSeries = [ let pendingSeries = [
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
{ {
label: "Runtime", label: "Runtime",
value: (u, ts, sidx, didx) => value: (u, ts, sidx, didx) =>
@@ -161,87 +84,108 @@
} }
]; ];
// Y // Y
// if (useStatsSeries) { for (let i = 0; i < metricData.length; i++) {
// pendingSeries.push({ pendingSeries.push({
// label: "min", label: publicMode ? null : `${metricData[i]?.name} (${metricData[i]?.unit?.prefix}${metricData[i]?.unit?.base})`,
// scale: "y", scale: `y${i+1}`,
// width: lineWidth, width: lineWidth,
// stroke: cbmode ? "rgb(0,255,0)" : "red", stroke: fixedLineColors[i],
// }); });
// pendingSeries.push({
// label: "max",
// scale: "y",
// width: lineWidth,
// stroke: cbmode ? "rgb(0,0,255)" : "green",
// });
// pendingSeries.push({
// label: usesMeanStatsSeries ? "mean" : "median",
// scale: "y",
// width: lineWidth,
// stroke: "black",
// });
// } else {
for (let i = 0; i < metricData.length; i++) {
// Default
// if (!extendedLegendData) {
pendingSeries.push({
label: publicMode ? null : `${metricData[i]?.name} (${metricData[i]?.unit?.prefix}${metricData[i]?.unit?.base})`,
scale: `y${i+1}`,
width: lineWidth,
stroke: fixedLineColors[i],
});
// }
// Extended Legend For NodeList
// else {
// pendingSeries.push({
// label:
// scope === "node"
// ? series[i].hostname
// : scope === "accelerator"
// ? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
// : scope + " #" + (i + 1),
// scale: "y",
// width: lineWidth,
// stroke: lineColor(i, series?.length),
// values: (u, sidx, idx) => {
// // "i" = "sidx - 1" : sidx contains x-axis-data
// if (idx == null)
// return {
// time: '-',
// value: '-',
// user: '-',
// job: '-'
// };
// if (series[i].id in extendedLegendData) {
// return {
// time: formatDurationTime(plotData[0][idx], forNode),
// value: plotData[sidx][idx],
// user: extendedLegendData[series[i].id].user,
// job: extendedLegendData[series[i].id].job,
// };
// } else {
// return {
// time: formatDurationTime(plotData[0][idx], forNode),
// value: plotData[sidx][idx],
// user: '-',
// job: '-',
// };
// }
// }
// });
// }
// };
}; };
return pendingSeries; return pendingSeries;
}) })
/* Effects */ // Set Options
// $effect(() => { function getOpts(optWidth, optHeight) {
// if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true; let baseOpts = {
// }) width: optWidth,
height: optHeight,
series: plotSeries,
axes: [
{
scale: "x",
incrs: timeIncrs(timestep, maxX, forNode),
values: (_, vals) => vals.map((v) => formatDurationTime(v, forNode)),
},
{
scale: "y1",
grid: { show: true },
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
{
side: 1,
scale: "y2",
grid: { show: false },
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
// bands: plotBands,
padding: [5, 10, -20, 0],
hooks: {},
scales: {
x: { time: false },
y1: { auto: true },
y2: { auto: true },
},
legend: {
show: !publicMode,
live: !publicMode
},
cursor: {
drag: { x: true, y: true },
}
}
if (publicMode) {
// X
baseOpts.axes[0].space = 60;
baseOpts.axes[0].font = '16px Arial';
// Y1
baseOpts.axes[1].space = 50;
baseOpts.axes[1].size = 60;
baseOpts.axes[1].font = '16px Arial';
baseOpts.axes[1].stroke = fixedLineColors[0];
// Y2
baseOpts.axes[2].space = 40;
baseOpts.axes[2].size = 60;
baseOpts.axes[2].font = '16px Arial';
baseOpts.axes[2].stroke = fixedLineColors[1];
} else {
baseOpts.title = 'Cluster Utilization';
baseOpts.plugins = [legendAsTooltipPlugin()];
// X
baseOpts.axes[0].label = 'Time';
// Y1
baseOpts.axes[1].label = `${metricData[0]?.name} (${metricData[0]?.unit?.prefix}${metricData[0]?.unit?.base})`;
// Y2
baseOpts.axes[2].label = `${metricData[1]?.name} (${metricData[1]?.unit?.prefix}${metricData[1]?.unit?.base})`;
baseOpts.hooks.draw = [
(u) => {
// Draw plot type label:
let textl = `Cluster ${cluster}`
let textr = `Sums of ${numNodes} nodes`
u.ctx.save();
u.ctx.textAlign = "start"; // 'end'
u.ctx.fillStyle = "black";
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10));
u.ctx.textAlign = "end";
u.ctx.fillStyle = "black";
u.ctx.fillText(
textr,
u.bbox.left + u.bbox.width - 10,
u.bbox.top + (forNode ? 0 : 10),
);
u.ctx.restore();
return;
},
]
}
return baseOpts;
};
/* Effects */
// This updates plot on all size changes if wrapper (== data) exists // This updates plot on all size changes if wrapper (== data) exists
$effect(() => { $effect(() => {
if (plotWrapper) { if (plotWrapper) {
@@ -262,73 +206,6 @@
} }
} }
// removed arg "subcluster": input metricconfig and topology now directly derived from subcluster
// function findJobAggregationThresholds(
// subClusterTopology,
// metricConfig,
// scope,
// numhwthreads,
// numaccs
// ) {
// if (!subClusterTopology || !metricConfig || !scope) {
// console.warn("Argument missing for findJobAggregationThresholds!");
// return null;
// }
// // handle special *-stat scopes
// if (scope.match(/(.*)-stat$/)) {
// const statParts = scope.split('-');
// scope = statParts[0]
// }
// if (metricConfig?.aggregation == "avg") {
// // Return as Configured
// return {
// normal: metricConfig.normal,
// caution: metricConfig.caution,
// alert: metricConfig.alert,
// peak: metricConfig.peak,
// };
// }
// if (metricConfig?.aggregation == "sum") {
// // Scale Thresholds
// let fraction;
// if (numaccs > 0) fraction = subClusterTopology.accelerators.length / numaccs;
// else if (numhwthreads > 0) fraction = subClusterTopology.core.length / numhwthreads;
// else fraction = 1; // Fallback
// let divisor;
// // Exclusive: Fraction = 1; Shared: Fraction > 1
// if (scope == 'node') divisor = fraction;
// // Cap divisor at number of available sockets or domains
// else if (scope == 'socket') divisor = (fraction < subClusterTopology.socket.length) ? subClusterTopology.socket.length : fraction;
// else if (scope == "memoryDomain") divisor = (fraction < subClusterTopology.memoryDomain.length) ? subClusterTopology.socket.length : fraction;
// // Use Maximum Division for Smallest Scopes
// else if (scope == "core") divisor = subClusterTopology.core.length;
// else if (scope == "hwthread") divisor = subClusterTopology.core.length; // alt. name for core
// else if (scope == "accelerator") divisor = subClusterTopology.accelerators.length;
// else {
// console.log('Unknown scope, return default aggregation thresholds for sum', scope)
// divisor = 1;
// }
// return {
// peak: metricConfig.peak / divisor,
// normal: metricConfig.normal / divisor,
// caution: metricConfig.caution / divisor,
// alert: metricConfig.alert / divisor,
// };
// }
// console.warn(
// "Missing or unkown aggregation mode (sum/avg) for metric:",
// metricConfig,
// );
// return null;
// }
// UPLOT PLUGIN // converts the legend into a simple tooltip // UPLOT PLUGIN // converts the legend into a simple tooltip
function legendAsTooltipPlugin({ function legendAsTooltipPlugin({
className, className,
@@ -408,220 +285,22 @@
} }
} }
// RETURN BG COLOR FROM THRESHOLD function onSizeChange(chgWidth, chgHeight) {
// function backgroundColor() {
// if (
// clusterCockpitConfig.plotConfiguration_colorBackground == false ||
// // !thresholds ||
// !(series && series.every((s) => s.statistics != null))
// )
// return backgroundColors.normal;
// let cond =
// thresholds.alert < thresholds.caution
// ? (a, b) => a <= b
// : (a, b) => a >= b;
// let avg =
// series.reduce((sum, series) => sum + series.statistics.avg, 0) /
// series.length;
// if (Number.isNaN(avg)) return backgroundColors.normal;
// if (cond(avg, thresholds.alert)) return backgroundColors.alert;
// if (cond(avg, thresholds.caution)) return backgroundColors.caution;
// return backgroundColors.normal;
// }
function render(ren_width, ren_height) {
// Set Options
const opts = {
width: ren_width,
height: ren_height,
title: publicMode ? null : 'Cluster Utilization',
plugins: [legendAsTooltipPlugin()],
series: plotSeries,
axes: [
{
scale: "x",
space: publicMode ? 60 : null,
incrs: timeIncrs(timestep, maxX, forNode),
font: publicMode ? '16px Arial' : CanvasRenderingContext2D['font'],
label: publicMode ? null : "Time",
values: (_, vals) => vals.map((v) => formatDurationTime(v, forNode)),
},
{
scale: "y1",
grid: { show: true },
stroke: publicMode ? fixedLineColors[0] : "#000000",
size: publicMode ? 60 : null,
space: publicMode ? 50 : null,
font: publicMode ? '16px Arial' : CanvasRenderingContext2D['font'],
label: publicMode ? null : `${metricData[0]?.name} (${metricData[0]?.unit?.prefix}${metricData[0]?.unit?.base})`,
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
{
side: 1,
scale: "y2",
grid: { show: false },
stroke: publicMode ? fixedLineColors[1] : "#000000",
size: publicMode ? 60 : null,
space: publicMode ? 40 : null,
font: publicMode ? '16px Arial' : CanvasRenderingContext2D['font'],
label: publicMode ? null : `${metricData[1]?.name} (${metricData[1]?.unit?.prefix}${metricData[1]?.unit?.base})`,
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
// bands: plotBands,
padding: [5, 10, -20, 0],
hooks: {
// init: [
// (u) => {
// /* IF Zoom Enabled */
// if (resampleConfig && !forNode) {
// u.over.addEventListener("dblclick", (e) => {
// // console.log('Dispatch: Zoom Reset')
// onZoom({
// lastZoomState: {
// x: { time: false },
// y: { auto: true }
// }
// });
// });
// };
// },
// ],
draw: publicMode ? null : [
(u) => {
// Draw plot type label:
let textl = `Cluster ${cluster}`
// let textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
// useStatsSeries
// ? (usesMeanStatsSeries ? ": min/mean/max" : ": min/median/max")
// : metricConfig != null && scope != metricConfig.scope
// ? ` (${metricConfig.aggregation})`
// : ""
// }`;
let textr = `Sums of ${numNodes} nodes`
//let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
u.ctx.save();
u.ctx.textAlign = "start"; // 'end'
u.ctx.fillStyle = "black";
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10));
u.ctx.textAlign = "end";
u.ctx.fillStyle = "black";
u.ctx.fillText(
textr,
u.bbox.left + u.bbox.width - 10,
u.bbox.top + (forNode ? 0 : 10),
);
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
// if (!thresholds) {
u.ctx.restore();
return;
// }
// let y = u.valToPos(thresholds.normal, "y", true);
// u.ctx.save();
// u.ctx.lineWidth = lineWidth;
// u.ctx.strokeStyle = "#000000";
// u.ctx.setLineDash([5, 5]);
// u.ctx.beginPath();
// u.ctx.moveTo(u.bbox.left, y);
// u.ctx.lineTo(u.bbox.left + u.bbox.width, y);
// u.ctx.stroke();
// u.ctx.restore();
},
],
// setScale: [
// (u, key) => { // If ZoomResample is Configured && Not System/Node View
// if (resampleConfig && !forNode && key === 'x') {
// const numX = (u.series[0].idxs[1] - u.series[0].idxs[0])
// if (numX <= resampleTrigger && timestep !== resampleMinimum) {
// /* Get closest zoom level; prevents multiple iterative zoom requests for big zoom-steps (e.g. 600 -> 300 -> 120 -> 60) */
// // Which resolution to theoretically request to achieve 30 or more visible data points:
// const target = (numX * timestep) / resampleTrigger
// // Which configured resolution actually matches the closest to theoretical target:
// const closest = resampleResolutions.reduce(function(prev, curr) {
// return (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev);
// });
// // Prevents non-required dispatches
// if (timestep !== closest) {
// // console.log('Dispatch: Zoom with Res from / to', timestep, closest)
// onZoom({
// newRes: closest,
// lastZoomState: u?.scales,
// lastThreshold: thresholds?.normal
// });
// }
// } else {
// // console.log('Dispatch: Zoom Update States')
// onZoom({
// lastZoomState: u?.scales,
// lastThreshold: thresholds?.normal
// });
// };
// };
// },
// ]
},
scales: {
x: { time: false },
y1: { auto: true },
y2: { auto: true },
},
legend: {
// Display legend until max 12 Y-dataseries
show: true, // metricData.length <= 12 || useStatsSeries,
live: true // But This Plot always for 2 Data-Series
},
cursor: {
drag: { x: true, y: true },
}
};
// Handle Render
if (!uplot) {
// if (plotSync) {
// opts.cursor.sync = {
// key: plotSync.key,
// scales: ["x", null],
// }
// }
// if (zoomState && metricConfig?.aggregation == "avg") {
// opts.scales = {...zoomState}
// } else if (zoomState && metricConfig?.aggregation == "sum") {
// // Allow Zoom In === Ymin changed
// if (zoomState.y.min !== 0) { // scope change?: only use zoomState if thresholds match
// if ((thresholdState === thresholds?.normal)) { opts.scales = {...zoomState} };
// } // else: reset scaling to default
// }
uplot = new uPlot(opts, plotData, plotWrapper);
} else {
uplot.setSize({ width: ren_width, height: ren_height });
}
}
function onSizeChange(chg_width, chg_height) {
if (!uplot) return;
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null; timeoutId = null;
render(chg_width, chg_height); render(chgWidth, chgHeight);
}, renderSleepTime); }, renderSleepTime);
} }
/* On Mount */ function render(renWidth, renHeight) {
onMount(() => { if (!uplot) {
if (plotWrapper) { let opts = getOpts(renWidth, renHeight);
render(width, height); uplot = new uPlot(opts, plotData, plotWrapper);
} else {
uplot.setSize({ width: renWidth, height: renHeight });
} }
}); }
/* On Destroy */ /* On Destroy */
onDestroy(() => { onDestroy(() => {
@@ -636,8 +315,12 @@
<div bind:this={plotWrapper} bind:clientWidth={width} <div bind:this={plotWrapper} bind:clientWidth={width}
class={forNode ? 'py-2 rounded' : 'rounded'} class={forNode ? 'py-2 rounded' : 'rounded'}
></div> ></div>
{:else if cluster}
<Card body color="warning" class="mx-4"
>Cannot render plot: No series data returned for <code>{cluster}</code>.</Card
>
{:else} {:else}
<Card body color="warning" class="mx-4" <Card body color="warning" class="mx-4"
>Cannot render plot: No series data returned for <code>{cluster}</code></Card >Cannot render plot: No series data returned.</Card
> >
{/if} {/if}

View File

@@ -40,8 +40,7 @@
useColors = true, useColors = true,
useLegend = true, useLegend = true,
colorBackground = false, colorBackground = false,
width = 600, height = 300,
height = 380,
} = $props(); } = $props();
/* Const Init */ /* Const Init */
@@ -53,11 +52,12 @@
/* State Init */ /* State Init */
let plotWrapper = $state(null); let plotWrapper = $state(null);
let width = $state(0); // Wrapper Width
let uplot = $state(null); let uplot = $state(null);
/* Effect */ /* Effect */
$effect(() => { $effect(() => {
if (allowSizeChange) sizeChanged(width, height); if (allowSizeChange) onSizeChange(width, height);
}); });
// Copied Example Vars for Uplot Bubble // Copied Example Vars for Uplot Bubble
@@ -517,11 +517,11 @@
} }
// Main Functions // Main Functions
function sizeChanged() { function onSizeChange() {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null; timeoutId = null;
if (uplot) uplot.destroy(); if (uplot) uplot.destroy(); // Prevents Multi-Render
render(roofData, jobsData, nodesData); render(roofData, jobsData, nodesData);
}, 200); }, 200);
} }
@@ -995,7 +995,7 @@
</script> </script>
{#if roofData != null} {#if roofData != null}
<div bind:this={plotWrapper} class="p-2"></div> <div bind:this={plotWrapper} bind:clientWidth={width} class="p-2"></div>
{:else} {:else}
<Card class="mx-4 my-2" body color="warning">Cannot render roofline: No data!</Card> <Card class="mx-4 my-2" body color="warning">Cannot render roofline: No data!</Card>
{/if} {/if}

View File

@@ -2,7 +2,6 @@
@component Node State/Health Data Stacked Plot Component, based on uPlot; states by timestamp @component Node State/Health Data Stacked Plot Component, based on uPlot; states by timestamp
Properties: Properties:
- `width Number?`: The plot width [Default: 0]
- `height Number?`: The plot height [Default: 300] - `height Number?`: The plot height [Default: 300]
- `data [Array]`: The data object [Default: null] - `data [Array]`: The data object [Default: null]
- `xlabel String?`: Plot X axis label [Default: ""] - `xlabel String?`: Plot X axis label [Default: ""]
@@ -15,12 +14,11 @@
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
import { formatUnixTime } from "../units.js"; import { formatUnixTime } from "../units.js";
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap"; import { Card } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
width = 0,
height = 300, height = 300,
data = null, data = null,
xlabel = null, xlabel = null,
@@ -129,17 +127,17 @@
}; };
} }
function getStackedOpts(title, width, height, series, data) { function getStackedOpts(optTitle, optWidth, optHeight, optSeries, optData) {
let opts = { let opts = {
width, width: optWidth,
height, height: optHeight,
title, title: optTitle,
plugins: [legendAsTooltipPlugin()], plugins: [legendAsTooltipPlugin()],
series, series: optSeries,
axes: [ axes: [
{ {
scale: "x", scale: "x",
space: 25, // Tick Spacing // space: 25, // Tick Spacing
rotate: 30, rotate: 30,
show: true, show: true,
label: xlabel, label: xlabel,
@@ -168,25 +166,25 @@
} }
}; };
let stacked = stack(data, i => false); let stacked = stack(optData, i => false);
opts.bands = stacked.bands; opts.bands = stacked.bands;
opts.cursor = opts.cursor || {}; opts.cursor = opts.cursor || {};
opts.cursor.dataIdx = (u, seriesIdx, closestIdx, xValue) => { opts.cursor.dataIdx = (u, seriesIdx, closestIdx, xValue) => {
return data[seriesIdx][closestIdx] == null ? null : closestIdx; return optData[seriesIdx][closestIdx] == null ? null : closestIdx;
}; };
opts.series.forEach(s => { opts.series.forEach(s => {
// Format Time Info from Unix TS to LocalTimeString // Format Time Info from Unix TS to LocalTimeString
s.value = (u, v, si, i) => (si === 0) ? formatUnixTime(data[si][i]) : data[si][i]; s.value = (u, v, si, i) => (si === 0) ? formatUnixTime(optData[si][i]) : optData[si][i];
s.points = s.points || {}; s.points = s.points || {};
// scan raw unstacked data to return only real points // scan raw unstacked optData to return only real points
s.points.filter = (u, seriesIdx, show, gaps) => { s.points.filter = (u, seriesIdx, show, gaps) => {
if (show) { if (show) {
let pts = []; let pts = [];
data[seriesIdx].forEach((v, i) => { optData[seriesIdx].forEach((v, i) => {
v != null && pts.push(i); v != null && pts.push(i);
}); });
return pts; return pts;
@@ -206,7 +204,7 @@
opts.hooks = { opts.hooks = {
setSeries: [ setSeries: [
(u, i) => { (u, i) => {
let stacked = stack(data, i => !u.series[i].show); let stacked = stack(optData, i => !u.series[i].show);
u.delBand(null); u.delBand(null);
stacked.bands.forEach(b => u.addBand(b)); stacked.bands.forEach(b => u.addBand(b));
u.setData(stacked.data); u.setData(stacked.data);
@@ -298,10 +296,11 @@
/* Var Init */ /* Var Init */
let timeoutId = null; let timeoutId = null;
let uplot = null;
/* State Init */ /* State Init */
let plotWrapper = $state(null); let plotWrapper = $state(null);
let width = $state(0); // Wrapper Width
let uplot = $state(null);
/* Effects */ /* Effects */
$effect(() => { $effect(() => {
@@ -311,6 +310,14 @@
}); });
/* Functions */ /* Functions */
function onSizeChange(chg_width, chg_height) {
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
render(chg_width, chg_height);
}, 200);
}
function render(ren_width, ren_height) { function render(ren_width, ren_height) {
if (!uplot) { if (!uplot) {
let { opts, data } = getStackedOpts(title, ren_width, ren_height, plotSeries, collectData); let { opts, data } = getStackedOpts(title, ren_width, ren_height, plotSeries, collectData);
@@ -320,22 +327,6 @@
} }
} }
function onSizeChange(chg_width, chg_height) {
if (!uplot) return;
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
render(chg_width, chg_height);
}, 200);
}
/* On Mount */
onMount(() => {
if (plotWrapper) {
render(width, height);
}
});
/* On Destroy */ /* On Destroy */
onDestroy(() => { onDestroy(() => {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);

View File

@@ -57,7 +57,7 @@ export function formatUnixTime(t, withDate = false) {
return t; return t;
} else { } else {
if (withDate) return new Date(t * 1000).toLocaleString(); if (withDate) return new Date(t * 1000).toLocaleString();
else return new Date(t * 1000).toLocaleTimeString(); else return new Date(t * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
} }
} }
} }

View File

@@ -58,10 +58,6 @@
let to = $state(new Date(Date.now())); let to = $state(new Date(Date.now()));
let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400); let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
let colWidthJobs = $state(0); let colWidthJobs = $state(0);
let colWidthRoof = $state(0);
let colWidthTotals =$state(0);
let colWidthStacked1 = $state(0);
let colWidthStacked2 = $state(0);
/* Derived */ /* Derived */
// States for Stacked charts // States for Stacked charts
@@ -531,13 +527,11 @@
</Col> </Col>
<Col> <!-- Job Roofline --> <Col> <!-- Job Roofline -->
<div bind:clientWidth={colWidthRoof}> <div>
{#key $statusQuery?.data?.jobsMetricStats} {#key $statusQuery?.data?.jobsMetricStats}
<Roofline <Roofline
useColors={true} useColors={true}
allowSizeChange allowSizeChange
width={colWidthRoof}
height={300}
subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null} subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null}
roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats)} roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats)}
jobsData={transformJobsStatsToInfo($statusQuery?.data?.jobsMetricStats)} jobsData={transformJobsStatsToInfo($statusQuery?.data?.jobsMetricStats)}
@@ -547,24 +541,23 @@
</Col> </Col>
<Col> <!-- Total Cluster Metric in Time SUMS--> <Col> <!-- Total Cluster Metric in Time SUMS-->
<div bind:clientWidth={colWidthTotals}> <div>
<DoubleMetric {#key $statusQuery?.data?.clusterMetrics}
width={colWidthTotals} <DoubleMetric
timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60} timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60}
numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0} numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0}
metricData={$statusQuery?.data?.clusterMetrics?.metrics || []} metricData={$statusQuery?.data?.clusterMetrics?.metrics || []}
cluster={presetCluster} cluster={presetCluster}
fixLinewidth={2} />
/> {/key}
</div> </div>
</Col> </Col>
<Col> <!-- Stacked SchedState --> <Col> <!-- Stacked SchedState -->
<div bind:clientWidth={colWidthStacked1}> <div>
{#key $statesTimed?.data?.nodeStates} {#key $statesTimed?.data?.nodeStates}
<Stacked <Stacked
data={$statesTimed?.data?.nodeStates} data={$statesTimed?.data?.nodeStates}
width={colWidthStacked1}
height={330} height={330}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"
@@ -577,11 +570,10 @@
</Col> </Col>
<Col> <!-- Stacked Healthstate --> <Col> <!-- Stacked Healthstate -->
<div bind:clientWidth={colWidthStacked2}> <div>
{#key $statesTimed?.data?.healthStates} {#key $statesTimed?.data?.healthStates}
<Stacked <Stacked
data={$statesTimed?.data?.healthStates} data={$statesTimed?.data?.healthStates}
width={colWidthStacked2}
height={330} height={330}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"

View File

@@ -42,9 +42,6 @@
/* State Init */ /* State Init */
let pieWidth = $state(0); let pieWidth = $state(0);
let stackedWidth1 = $state(0);
let stackedWidth2 = $state(0);
let plotWidths = $state([]);
let from = $state(new Date(Date.now() - 5 * 60 * 1000)); let from = $state(new Date(Date.now() - 5 * 60 * 1000));
let to = $state(new Date(Date.now())); let to = $state(new Date(Date.now()));
let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400); let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
@@ -414,14 +411,13 @@
{#if $statesTimed.data} {#if $statesTimed.data}
<Row cols={{ md: 2 , sm: 1}} class="mb-3 justify-content-center"> <Row cols={{ md: 2 , sm: 1}} class="mb-3 justify-content-center">
<Col class="px-3 mt-2 mt-lg-0"> <Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={stackedWidth1}> <div>
{#key $statesTimed?.data?.nodeStates} {#key $statesTimed?.data?.nodeStates}
<h4 class="text-center"> <h4 class="text-center">
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States Over Time {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States Over Time
</h4> </h4>
<Stacked <Stacked
data={$statesTimed?.data?.nodeStates} data={$statesTimed?.data?.nodeStates}
width={stackedWidth1 * 0.95}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"
yunit = "#Count" yunit = "#Count"
@@ -432,14 +428,13 @@
</div> </div>
</Col> </Col>
<Col class="px-3 mt-2 mt-lg-0"> <Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={stackedWidth2}> <div>
{#key $statesTimed?.data?.healthStates} {#key $statesTimed?.data?.healthStates}
<h4 class="text-center"> <h4 class="text-center">
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Health States Over Time {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Health States Over Time
</h4> </h4>
<Stacked <Stacked
data={$statesTimed?.data?.healthStates} data={$statesTimed?.data?.healthStates}
width={stackedWidth2 * 0.95}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"
yunit = "#Count" yunit = "#Count"
@@ -628,13 +623,11 @@
</Card> </Card>
</Col> </Col>
<Col class="px-3 mt-2 mt-lg-0"> <Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={plotWidths[i]}> <div>
{#key $statusQuery?.data?.nodeMetrics} {#key $statusQuery?.data?.nodeMetrics}
<Roofline <Roofline
useColors={true} useColors={true}
allowSizeChange allowSizeChange
width={plotWidths[i] - 10}
height={300}
cluster={cluster} cluster={cluster}
subCluster={subCluster} subCluster={subCluster}
roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics.filter( roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics.filter(
@@ -650,13 +643,11 @@
</div> </div>
</Col> </Col>
<Col class="px-3 mt-2 mt-lg-0"> <Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={plotWidths[i]}> <div>
{#key $statusQuery?.data?.jobsMetricStats} {#key $statusQuery?.data?.jobsMetricStats}
<Roofline <Roofline
useColors={true} useColors={true}
allowSizeChange allowSizeChange
width={plotWidths[i] - 10}
height={300}
subCluster={subCluster} subCluster={subCluster}
roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats.filter( roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats.filter(
(data) => data.subCluster == subCluster.name, (data) => data.subCluster == subCluster.name,