Migrate MetricPlot component

This commit is contained in:
Christoph Kluge 2025-07-01 15:50:45 +02:00
parent 56e3f2da5c
commit aa8789f8f8
3 changed files with 364 additions and 341 deletions

View File

@ -201,7 +201,7 @@
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
{#if metric.disabled == false && metric.data}
<MetricPlot
on:zoom={({detail}) => handleZoom(detail, metric.data.name)}
onZoom={(detail) => handleZoom(detail, metric.data.name)}
height={plotHeight}
timestep={metric.data.metric.timestep}
scope={metric.data.scope}

View File

@ -20,7 +20,243 @@
- `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null]
-->
<script context="module">
<script>
import uPlot from "uplot";
import { formatNumber, formatTime } from "../units.js";
import { getContext, onMount, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */
let {
metric,
scope = "node",
width = 0,
height = 300,
timestep,
series,
useStatsSeries = false,
statisticsSeries = null,
cluster = "",
subCluster,
isShared = false,
forNode = false,
numhwthreads = 0,
numaccs = 0,
zoomState = null,
thresholdState = null,
extendedLegendData = null,
onZoom
} = $props();
/* Const Init */
const clusterCockpitConfig = getContext("cc-config");
const resampleConfig = getContext("resampling");
const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
const lineWidth = clusterCockpitConfig?.plot_general_lineWidth / window.devicePixelRatio || 2;
const lineColors = clusterCockpitConfig?.plot_general_colorscheme || ["#00bfff","#0000ff","#ff00ff","#ff0000","#ff8000","#ffff00","#80ff00"];
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
const renderSleepTime = 200;
const normalLineColor = "#000000";
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 */
let timeoutId = null;
/* State Init */
let plotWrapper = $state(null);
let uplot = $state(null);
/* Derived */
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,
isShared,
numhwthreads,
numaccs
));
const longestSeries = $derived.by(() => {
if (useStatsSeries) {
return usesMeanStatsSeries ? statisticsSeries?.mean?.length : statisticsSeries?.median?.length;
} else {
return series.reduce((n, series) => Math.max(n, series.data.length), 0);
}
});
const maxX = $derived(longestSeries * timestep);
const maxY = $derived.by(() => {
let pendingY = 0;
if (useStatsSeries) {
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) {
// Hard y-range render limit if outliers in series data
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)];
// X
if (forNode === true) {
// Negative Timestamp Buildup
for (let i = 0; i <= longestSeries; i++) {
pendingData[0][i] = (longestSeries - i) * timestep * -1;
}
} else {
// Positive Timestamp Buildup
for (let j = 0; j < longestSeries; j++) {
pendingData[0][j] = j * timestep;
};
};
// Y
if (useStatsSeries) {
pendingData.push(statisticsSeries.min);
pendingData.push(statisticsSeries.max);
if (usesMeanStatsSeries) {
pendingData.push(statisticsSeries.mean);
} else {
pendingData.push(statisticsSeries.median);
}
} else {
for (let i = 0; i < series.length; i++) {
pendingData.push(series[i].data);
};
};
return pendingData;
})
const plotSeries = $derived.by(() => {
let pendingSeries = [
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
{
label: "Runtime",
value: (u, ts, sidx, didx) =>
(didx == null) ? null : formatTime(ts, forNode),
}
];
// Y
if (useStatsSeries) {
pendingSeries.push({
label: "min",
scale: "y",
width: lineWidth,
stroke: cbmode ? "rgb(0,255,0)" : "red",
});
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 < series?.length; i++) {
// Default
if (!extendedLegendData) {
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),
});
}
// 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: formatTime(plotData[0][idx], forNode),
value: plotData[sidx][idx],
user: extendedLegendData[series[i].id].user,
job: extendedLegendData[series[i].id].job,
};
} else {
return {
time: formatTime(plotData[0][idx], forNode),
value: plotData[sidx][idx],
user: '-',
job: '-',
};
}
}
});
}
};
};
return pendingSeries;
})
/* Effects */
$effect(() => {
if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
})
// This updates plot on all size changes if wrapper (== data) exists
$effect(() => {
if (plotWrapper) {
onSizeChange(width, height);
}
});
/* Functions */
function timeIncrs(timestep, maxX, forNode) {
if (forNode === true) {
return [60, 120, 240, 300, 360, 480, 600, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
@ -66,7 +302,6 @@
};
}
if (metricConfig?.aggregation == "sum") {
let divisor;
if (isShared == true) { // Shared
@ -98,70 +333,6 @@
);
return null;
}
</script>
<script>
import uPlot from "uplot";
import { formatNumber, formatTime } from "../units.js";
import { getContext, onMount, onDestroy, createEventDispatcher } from "svelte";
import { Card } from "@sveltestrap/sveltestrap";
export let metric;
export let scope = "node";
export let width = 0;
export let height = 300;
export let timestep;
export let series;
export let useStatsSeries = false;
export let statisticsSeries = null;
export let cluster = "";
export let subCluster;
export let isShared = false;
export let forNode = false;
export let numhwthreads = 0;
export let numaccs = 0;
export let zoomState = null;
export let thresholdState = null;
export let extendedLegendData = null;
if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
const usesMeanStatsSeries = (statisticsSeries?.mean && statisticsSeries.mean.length != 0)
const dispatch = createEventDispatcher();
const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
const clusterCockpitConfig = getContext("cc-config");
const renderSleepTime = 200;
const normalLineColor = "#000000";
const lineWidth =
clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
const lineColors = clusterCockpitConfig.plot_general_colorscheme;
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
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)",
};
const thresholds = findJobAggregationThresholds(
subClusterTopology,
metricConfig,
scope,
isShared,
numhwthreads,
numaccs
);
const resampleConfig = getContext("resampling");
let resampleTrigger;
let resampleResolutions;
let resampleMinimum;
if (resampleConfig) {
resampleTrigger = Number(resampleConfig.trigger)
resampleResolutions = [...resampleConfig.resolutions];
resampleMinimum = Math.min(...resampleConfig.resolutions);
}
// UPLOT PLUGIN // converts the legend into a simple tooltip
function legendAsTooltipPlugin({
@ -266,153 +437,13 @@
return backgroundColors.normal;
}
// PREPARE UPLOT ...
function lineColor(i, n) {
if (n >= lineColors.length) return lineColors[i % lineColors.length];
if (n && n >= lineColors.length) return lineColors[i % lineColors.length];
else return lineColors[Math.floor((i / n) * lineColors.length)];
}
const longestSeries = useStatsSeries
? (usesMeanStatsSeries ? statisticsSeries.mean.length : statisticsSeries.median.length)
: series.reduce((n, series) => Math.max(n, series.data.length), 0);
const maxX = longestSeries * timestep;
let maxY = null;
if (thresholds !== null) {
maxY = useStatsSeries
? statisticsSeries.max.reduce(
(max, x) => Math.max(max, x),
thresholds.normal,
) || thresholds.normal
: series.reduce(
(max, series) => Math.max(max, series.statistics?.max),
thresholds.normal,
) || thresholds.normal;
if (maxY >= 10 * thresholds.peak) {
// Hard y-range render limit if outliers in series data
maxY = 10 * thresholds.peak;
}
}
const plotData = [new Array(longestSeries)];
if (forNode === true) {
// Negative Timestamp Buildup
for (let i = 0; i <= longestSeries; i++) {
plotData[0][i] = (longestSeries - i) * timestep * -1;
}
} else {
// Positive Timestamp Buildup
for (
let j = 0;
j < longestSeries;
j++ // TODO: Cache/Reuse this array?
)
plotData[0][j] = j * timestep;
}
const plotSeries = [
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
{
label: "Runtime",
value: (u, ts, sidx, didx) =>
(didx == null) ? null : formatTime(ts, forNode),
}
];
let plotBands = undefined;
if (useStatsSeries) {
plotData.push(statisticsSeries.min);
plotData.push(statisticsSeries.max);
if (usesMeanStatsSeries) {
plotData.push(statisticsSeries.mean);
} else {
plotData.push(statisticsSeries.median);
}
plotSeries.push({
label: "min",
scale: "y",
width: lineWidth,
stroke: cbmode ? "rgb(0,255,0)" : "red",
});
plotSeries.push({
label: "max",
scale: "y",
width: lineWidth,
stroke: cbmode ? "rgb(0,0,255)" : "green",
});
plotSeries.push({
label: usesMeanStatsSeries ? "mean" : "median",
scale: "y",
width: lineWidth,
stroke: "black",
});
plotBands = [
{ 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)" },
];
} else {
for (let i = 0; i < series.length; i++) {
plotData.push(series[i].data);
// Default
if (!extendedLegendData) {
plotSeries.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),
});
}
// Extended Legend For NodeList
else {
plotSeries.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: formatTime(plotData[0][idx], forNode),
value: plotData[sidx][idx],
user: extendedLegendData[series[i].id].user,
job: extendedLegendData[series[i].id].job,
};
} else {
return {
time: formatTime(plotData[0][idx], forNode),
value: plotData[sidx][idx],
user: '-',
job: '-',
};
}
}
});
}
}
}
function render(ren_width, ren_height) {
// Set Options
const opts = {
width,
height,
@ -438,10 +469,10 @@
init: [
(u) => {
/* IF Zoom Enabled */
if (resampleConfig) {
if (resampleConfig && !forNode) {
u.over.addEventListener("dblclick", (e) => {
// console.log('Dispatch: Zoom Reset')
dispatch('zoom', {
onZoom({
lastZoomState: {
x: { time: false },
y: { auto: true }
@ -507,7 +538,7 @@
// Prevents non-required dispatches
if (timestep !== closest) {
// console.log('Dispatch: Zoom with Res from / to', timestep, closest)
dispatch('zoom', {
onZoom({
newRes: closest,
lastZoomState: u?.scales,
lastThreshold: thresholds?.normal
@ -515,7 +546,7 @@
}
} else {
// console.log('Dispatch: Zoom Update States')
dispatch('zoom', {
onZoom({
lastZoomState: u?.scales,
lastThreshold: thresholds?.normal
});
@ -538,12 +569,7 @@
}
};
// RENDER HANDLING
let plotWrapper = null;
let uplot = null;
let timeoutId = null;
function render(ren_width, ren_height) {
// Handle Render
if (!uplot) {
opts.width = ren_width;
opts.height = ren_height;
@ -570,22 +596,19 @@
}, renderSleepTime);
}
/* On Mount */
onMount(() => {
if (plotWrapper) {
render(width, height);
}
});
/* On Destroy */
onDestroy(() => {
if (timeoutId != null) clearTimeout(timeoutId);
if (uplot) uplot.destroy();
});
// This updates plot on all size changes if wrapper (== data) exists
$: if (plotWrapper) {
onSizeChange(width, height);
}
</script>
<!-- Define $width Wrapper and NoData Card -->

View File

@ -29,7 +29,7 @@
import {
minScope,
} from "../generic/utils.js";
import Timeseries from "../generic/plots/MetricPlot.svelte";
import MetricPlot from "../generic/plots/MetricPlot.svelte";
/* Svelte 5 Props */
let {
@ -173,8 +173,8 @@
{:else if $metricData.error}
<Card body color="danger">{$metricData.error.message}</Card>
{:else if selectedSeries != null && !patternMatches}
<Timeseries
on:zoom={({detail}) => handleZoom(detail)}
<MetricPlot
onZoom={(detail) => handleZoom(detail)}
cluster={job.cluster}
subCluster={job.subCluster}
timestep={selectedData.timestep}
@ -188,8 +188,8 @@
{thresholdState}
/>
{:else if statsSeries[selectedScopeIndex] != null && patternMatches}
<Timeseries
on:zoom={({detail}) => handleZoom(detail)}
<MetricPlot
onZoom={(detail) => handleZoom(detail)}
cluster={job.cluster}
subCluster={job.subCluster}
timestep={selectedData.timestep}