Migrate jobCompare and comparison plot

This commit is contained in:
Christoph Kluge 2025-06-20 15:20:26 +02:00
parent 1e039cb1bf
commit dceb92ba8e
2 changed files with 181 additions and 170 deletions

View File

@ -23,94 +23,91 @@
import { formatTime, roundTwoDigits } from "./units.js"; import { formatTime, roundTwoDigits } from "./units.js";
import Comparogram from "./plots/Comparogram.svelte"; import Comparogram from "./plots/Comparogram.svelte";
const ccconfig = getContext("cc-config"), /* Svelte 5 Props */
// initialized = getContext("initialized"), let {
globalMetrics = getContext("globalMetrics"); matchedCompareJobs = $bindable(0),
metrics = $bindable(ccconfig?.plot_list_selectedMetrics),
filterBuffer = [],
} = $props();
export let matchedCompareJobs = 0; /* Const Init */
export let metrics = ccconfig.plot_list_selectedMetrics; const client = getContextClient();
export let filterBuffer = []; const ccconfig = getContext("cc-config");
const globalMetrics = getContext("globalMetrics");
let filter = [...filterBuffer] || []; // const initialized = getContext("initialized");
let comparePlotData = {};
let compareTableData = [];
let compareTableSorting = {};
let jobIds = [];
let jobClusters = [];
let tableJobIDFilter = "";
/*uPlot*/
let plotSync = uPlot.sync("compareJobsView");
/* GQL */
const client = getContextClient();
// Pull All Series For Metrics Statistics Only On Node Scope // Pull All Series For Metrics Statistics Only On Node Scope
const compareQuery = gql` const compareQuery = gql`
query ($filter: [JobFilter!]!, $metrics: [String!]!) { query ($filter: [JobFilter!]!, $metrics: [String!]!) {
jobsMetricStats(filter: $filter, metrics: $metrics) { jobsMetricStats(filter: $filter, metrics: $metrics) {
id id
jobId jobId
startTime startTime
duration duration
cluster cluster
subCluster subCluster
numNodes numNodes
numHWThreads numHWThreads
numAccelerators numAccelerators
stats { stats {
name name
data { data {
min min
avg avg
max max
}
} }
} }
} }
}
`; `;
/* REACTIVES */ /* Var Init*/
let plotSync = uPlot.sync("compareJobsView");
$: compareData = queryStore({ /* State Init */
client: client, let filter = $state([...filterBuffer] || []);
query: compareQuery, let tableJobIDFilter = $state("");
variables:{ filter, metrics },
});
$: matchedCompareJobs = $compareData.data != null ? $compareData.data.jobsMetricStats.length : -1; /* Derived*/
const compareData = $derived(queryStore({
$: if ($compareData.data != null) { client: client,
jobIds = []; query: compareQuery,
jobClusters = []; variables:{ filter, metrics },
comparePlotData = {}; })
compareTableData = [...$compareData.data.jobsMetricStats]; );
jobs2uplot($compareData.data.jobsMetricStats, metrics); let jobIds = $derived($compareData?.data ? $compareData.data.jobsMetricStats.map((jms) => jms.jobId) : []);
} let jobClusters = $derived($compareData?.data ? $compareData.data.jobsMetricStats.map((jms) => `${jms.cluster} ${jms.subCluster}`) : []);
let compareTableData = $derived($compareData?.data ? [...$compareData.data.jobsMetricStats] : []);
$: if ((!$compareData.fetching && !$compareData.error) && metrics) { let comparePlotData = $derived($compareData?.data ? jobs2uplot($compareData.data.jobsMetricStats, metrics) : {});
let compareTableSorting = $derived.by(() => {
let pendingSort = {};
// Meta // Meta
compareTableSorting['meta'] = { pendingSort['meta'] = {
startTime: { dir: "down", active: true }, startTime: { dir: "down", active: true },
duration: { dir: "up", active: false }, duration: { dir: "up", active: false },
cluster: { dir: "up", active: false }, cluster: { dir: "up", active: false },
}; };
// Resources // Resources
compareTableSorting['resources'] = { pendingSort['resources'] = {
Nodes: { dir: "up", active: false }, Nodes: { dir: "up", active: false },
Threads: { dir: "up", active: false }, Threads: { dir: "up", active: false },
Accs: { dir: "up", active: false }, Accs: { dir: "up", active: false },
}; };
// Metrics
for (let metric of metrics) { for (let metric of metrics) {
compareTableSorting[metric] = { pendingSort[metric] = {
min: { dir: "up", active: false }, min: { dir: "up", active: false },
avg: { dir: "up", active: false }, avg: { dir: "up", active: false },
max: { dir: "up", active: false }, max: { dir: "up", active: false },
}; };
} }
} return pendingSort;
});
/* FUNCTIONS */ /* Effect */
$effect(() => {
matchedCompareJobs = $compareData?.data != null ? $compareData.data.jobsMetricStats.length : -1;
});
/* Functions */
// (Re-)query and optionally set new filters; Query will be started reactively. // (Re-)query and optionally set new filters; Query will be started reactively.
export function queryJobs(filters) { export function queryJobs(filters) {
if (filters != null) { if (filters != null) {
@ -178,42 +175,42 @@
} }
function jobs2uplot(jobs, metrics) { function jobs2uplot(jobs, metrics) {
// Proxy Init
let pendingComparePlotData = {};
// Resources Init // Resources Init
comparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS] pendingComparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS]
// Metric Init // Metric Init
for (let m of metrics) { for (let m of metrics) {
// Get Unit // Get Unit
const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit
const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "") const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
comparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX] pendingComparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX]
} }
// Iterate jobs if exists // Iterate jobs if exists
if (jobs) { if (jobs) {
let plotIndex = 0 let plotIndex = 0
jobs.forEach((j) => { jobs.forEach((j) => {
// Collect JobIDs & Clusters for X-Ticks and Legend
jobIds.push(j.jobId)
jobClusters.push(`${j.cluster} ${j.subCluster}`)
// Resources // Resources
comparePlotData['resources'].data[0].push(plotIndex) pendingComparePlotData['resources'].data[0].push(plotIndex)
comparePlotData['resources'].data[1].push(j.startTime) pendingComparePlotData['resources'].data[1].push(j.startTime)
comparePlotData['resources'].data[2].push(j.duration) pendingComparePlotData['resources'].data[2].push(j.duration)
comparePlotData['resources'].data[3].push(j.numNodes) pendingComparePlotData['resources'].data[3].push(j.numNodes)
comparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0) pendingComparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
comparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0) pendingComparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
// Metrics // Metrics
for (let s of j.stats) { for (let s of j.stats) {
comparePlotData[s.name].data[0].push(plotIndex) pendingComparePlotData[s.name].data[0].push(plotIndex)
comparePlotData[s.name].data[1].push(j.startTime) pendingComparePlotData[s.name].data[1].push(j.startTime)
comparePlotData[s.name].data[2].push(j.duration) pendingComparePlotData[s.name].data[2].push(j.duration)
comparePlotData[s.name].data[3].push(s.data.min) pendingComparePlotData[s.name].data[3].push(s.data.min)
comparePlotData[s.name].data[4].push(s.data.avg) pendingComparePlotData[s.name].data[4].push(s.data.avg)
comparePlotData[s.name].data[5].push(s.data.max) pendingComparePlotData[s.name].data[5].push(s.data.max)
} }
plotIndex++ plotIndex++
}) })
} }
return {...pendingComparePlotData};
} }
// Adapt for Persisting Job Selections in DB later down the line // Adapt for Persisting Job Selections in DB later down the line
@ -242,7 +239,6 @@
// } // }
// }); // });
// } // }
</script> </script>
{#if $compareData.fetching} {#if $compareData.fetching}
@ -269,7 +265,7 @@
xticks={jobIds} xticks={jobIds}
xinfo={jobClusters} xinfo={jobClusters}
ylabel={'Resource Counts'} ylabel={'Resource Counts'}
data={comparePlotData['resources'].data} data={comparePlotData['resources']?.data}
{plotSync} {plotSync}
forResources forResources
/> />
@ -285,8 +281,8 @@
xinfo={jobClusters} xinfo={jobClusters}
ylabel={m} ylabel={m}
metric={m} metric={m}
yunit={comparePlotData[m].unit} yunit={comparePlotData[m]?.unit}
data={comparePlotData[m].data} data={comparePlotData[m]?.data}
{plotSync} {plotSync}
/> />
</Col> </Col>
@ -318,7 +314,7 @@
</InputGroupText> </InputGroupText>
</InputGroup> </InputGroup>
</th> </th>
<th on:click={() => sortBy('meta', 'startTime')}> <th onclick={() => sortBy('meta', 'startTime')}>
Sort Sort
<Icon <Icon
name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime'] name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime']
@ -327,7 +323,7 @@
: ''}" : ''}"
/> />
</th> </th>
<th on:click={() => sortBy('meta', 'duration')}> <th onclick={() => sortBy('meta', 'duration')}>
Sort Sort
<Icon <Icon
name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration'] name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration']
@ -336,7 +332,7 @@
: ''}" : ''}"
/> />
</th> </th>
<th on:click={() => sortBy('meta', 'cluster')}> <th onclick={() => sortBy('meta', 'cluster')}>
Sort Sort
<Icon <Icon
name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster'] name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster']
@ -346,7 +342,7 @@
/> />
</th> </th>
{#each ["Nodes", "Threads", "Accs"] as res} {#each ["Nodes", "Threads", "Accs"] as res}
<th on:click={() => sortBy('resources', res)}> <th onclick={() => sortBy('resources', res)}>
{res} {res}
<Icon <Icon
name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res] name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res]
@ -358,7 +354,7 @@
{/each} {/each}
{#each metrics as metric} {#each metrics as metric}
{#each ["min", "avg", "max"] as stat} {#each ["min", "avg", "max"] as stat}
<th on:click={() => sortBy(metric, stat)}> <th onclick={() => sortBy(metric, stat)}>
{stat.charAt(0).toUpperCase() + stat.slice(1)} {stat.charAt(0).toUpperCase() + stat.slice(1)}
<Icon <Icon
name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat] name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat]

View File

@ -18,86 +18,30 @@
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onMount, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap"; import { Card } from "@sveltestrap/sveltestrap";
export let metric = "";
export let width = 0;
export let height = 300;
export let data = null;
export let xlabel = "";
export let xticks = [];
export let xinfo = [];
export let ylabel = "";
export let yunit = "";
export let title = "";
export let forResources = false;
export let plotSync;
// NOTE: Metric Thresholds non-required, Cluster Mixing Allowed // NOTE: Metric Thresholds non-required, Cluster Mixing Allowed
/* Svelte 5 Props */
let {
metric = "",
width = 0,
height = 300,
data = null,
xlabel = "",
xticks = [],
xinfo = [],
ylabel = "",
yunit = "",
title = "",
forResources = false,
plotSync,
} = $props();
/* Const Init */
const clusterCockpitConfig = getContext("cc-config"); const clusterCockpitConfig = getContext("cc-config");
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio; const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false; const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
// UPLOT PLUGIN // converts the legend into a simple tooltip // UPLOT SERIES INIT //
function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl;
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
legendEl.classList.remove("u-inline");
className && legendEl.classList.add(className);
uPlot.assign(legendEl.style, {
minWidth: "100px",
textAlign: "left",
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style,
});
// hide series color markers:
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none";
const overEl = u.over;
overEl.style.overflow = "visible";
// move legend into plot bounds
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {
legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
}
function update(u) {
const { left, top } = u.cursor;
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
legendEl.style.transform =
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
}
return {
hooks: {
init: init,
setCursor: update,
},
};
}
const plotSeries = [ const plotSeries = [
{ {
label: "JobID", label: "JobID",
@ -122,6 +66,7 @@
}, },
] ]
// UPLOT SCALES INIT //
if (forResources) { if (forResources) {
const resSeries = [ const resSeries = [
{ {
@ -177,11 +122,13 @@
plotSeries.push(...statsSeries) plotSeries.push(...statsSeries)
}; };
// UPLOT BAND COLORS //
const plotBands = [ const plotBands = [
{ series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" }, { series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
{ series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" }, { series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
]; ];
// UPLOT OPTIONS //
const opts = { const opts = {
width, width,
height, height,
@ -259,11 +206,83 @@
} }
}; };
// RENDER HANDLING /* Var Init */
let plotWrapper = null;
let uplot = null;
let timeoutId = null; let timeoutId = null;
let uplot = null;
/* State Init */
let plotWrapper = $state(null);
/* Effects */
$effect(() => {
if (plotWrapper) {
onSizeChange(width, height);
}
});
/* Functions */
// UPLOT PLUGIN // converts the legend into a simple tooltip
function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl;
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
legendEl.classList.remove("u-inline");
className && legendEl.classList.add(className);
uPlot.assign(legendEl.style, {
minWidth: "100px",
textAlign: "left",
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style,
});
// hide series color markers:
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none";
const overEl = u.over;
overEl.style.overflow = "visible";
// move legend into plot bounds
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {
legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
}
function update(u) {
const { left, top } = u.cursor;
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
legendEl.style.transform =
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
}
return {
hooks: {
init: init,
setCursor: update,
},
};
}
// RENDER HANDLING
function render(ren_width, ren_height) { function render(ren_width, ren_height) {
if (!uplot) { if (!uplot) {
opts.width = ren_width; opts.width = ren_width;
@ -284,22 +303,18 @@
}, 200); }, 200);
} }
/* On Mount */
onMount(() => { onMount(() => {
if (plotWrapper) { if (plotWrapper) {
render(width, height); render(width, height);
} }
}); });
/* On Destroy */
onDestroy(() => { onDestroy(() => {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
if (uplot) uplot.destroy(); if (uplot) uplot.destroy();
}); });
// This updates plot on all size changes if wrapper (== data) exists
$: if (plotWrapper) {
onSizeChange(width, height);
}
</script> </script>
<!-- Define $width Wrapper and NoData Card --> <!-- Define $width Wrapper and NoData Card -->