mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-01 03:03:50 +02:00
Migrate jobCompare and comparison plot
This commit is contained in:
parent
1e039cb1bf
commit
dceb92ba8e
@ -23,94 +23,91 @@
|
||||
import { formatTime, roundTwoDigits } from "./units.js";
|
||||
import Comparogram from "./plots/Comparogram.svelte";
|
||||
|
||||
const ccconfig = getContext("cc-config"),
|
||||
// initialized = getContext("initialized"),
|
||||
globalMetrics = getContext("globalMetrics");
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
matchedCompareJobs = $bindable(0),
|
||||
metrics = $bindable(ccconfig?.plot_list_selectedMetrics),
|
||||
filterBuffer = [],
|
||||
} = $props();
|
||||
|
||||
export let matchedCompareJobs = 0;
|
||||
export let metrics = ccconfig.plot_list_selectedMetrics;
|
||||
export let filterBuffer = [];
|
||||
|
||||
let filter = [...filterBuffer] || [];
|
||||
let comparePlotData = {};
|
||||
let compareTableData = [];
|
||||
let compareTableSorting = {};
|
||||
let jobIds = [];
|
||||
let jobClusters = [];
|
||||
let tableJobIDFilter = "";
|
||||
|
||||
/*uPlot*/
|
||||
let plotSync = uPlot.sync("compareJobsView");
|
||||
|
||||
/* GQL */
|
||||
const client = getContextClient();
|
||||
/* Const Init */
|
||||
const client = getContextClient();
|
||||
const ccconfig = getContext("cc-config");
|
||||
const globalMetrics = getContext("globalMetrics");
|
||||
// const initialized = getContext("initialized");
|
||||
// Pull All Series For Metrics Statistics Only On Node Scope
|
||||
const compareQuery = gql`
|
||||
query ($filter: [JobFilter!]!, $metrics: [String!]!) {
|
||||
jobsMetricStats(filter: $filter, metrics: $metrics) {
|
||||
id
|
||||
jobId
|
||||
startTime
|
||||
duration
|
||||
cluster
|
||||
subCluster
|
||||
numNodes
|
||||
numHWThreads
|
||||
numAccelerators
|
||||
stats {
|
||||
name
|
||||
data {
|
||||
min
|
||||
avg
|
||||
max
|
||||
query ($filter: [JobFilter!]!, $metrics: [String!]!) {
|
||||
jobsMetricStats(filter: $filter, metrics: $metrics) {
|
||||
id
|
||||
jobId
|
||||
startTime
|
||||
duration
|
||||
cluster
|
||||
subCluster
|
||||
numNodes
|
||||
numHWThreads
|
||||
numAccelerators
|
||||
stats {
|
||||
name
|
||||
data {
|
||||
min
|
||||
avg
|
||||
max
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/* REACTIVES */
|
||||
/* Var Init*/
|
||||
let plotSync = uPlot.sync("compareJobsView");
|
||||
|
||||
$: compareData = queryStore({
|
||||
client: client,
|
||||
query: compareQuery,
|
||||
variables:{ filter, metrics },
|
||||
});
|
||||
/* State Init */
|
||||
let filter = $state([...filterBuffer] || []);
|
||||
let tableJobIDFilter = $state("");
|
||||
|
||||
$: matchedCompareJobs = $compareData.data != null ? $compareData.data.jobsMetricStats.length : -1;
|
||||
|
||||
$: if ($compareData.data != null) {
|
||||
jobIds = [];
|
||||
jobClusters = [];
|
||||
comparePlotData = {};
|
||||
compareTableData = [...$compareData.data.jobsMetricStats];
|
||||
jobs2uplot($compareData.data.jobsMetricStats, metrics);
|
||||
}
|
||||
|
||||
$: if ((!$compareData.fetching && !$compareData.error) && metrics) {
|
||||
/* Derived*/
|
||||
const compareData = $derived(queryStore({
|
||||
client: client,
|
||||
query: compareQuery,
|
||||
variables:{ filter, 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] : []);
|
||||
let comparePlotData = $derived($compareData?.data ? jobs2uplot($compareData.data.jobsMetricStats, metrics) : {});
|
||||
let compareTableSorting = $derived.by(() => {
|
||||
let pendingSort = {};
|
||||
// Meta
|
||||
compareTableSorting['meta'] = {
|
||||
pendingSort['meta'] = {
|
||||
startTime: { dir: "down", active: true },
|
||||
duration: { dir: "up", active: false },
|
||||
cluster: { dir: "up", active: false },
|
||||
};
|
||||
// Resources
|
||||
compareTableSorting['resources'] = {
|
||||
pendingSort['resources'] = {
|
||||
Nodes: { dir: "up", active: false },
|
||||
Threads: { dir: "up", active: false },
|
||||
Accs: { dir: "up", active: false },
|
||||
};
|
||||
// Metrics
|
||||
for (let metric of metrics) {
|
||||
compareTableSorting[metric] = {
|
||||
pendingSort[metric] = {
|
||||
min: { dir: "up", active: false },
|
||||
avg: { 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.
|
||||
export function queryJobs(filters) {
|
||||
if (filters != null) {
|
||||
@ -178,42 +175,42 @@
|
||||
}
|
||||
|
||||
function jobs2uplot(jobs, metrics) {
|
||||
// Proxy Init
|
||||
let pendingComparePlotData = {};
|
||||
// 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
|
||||
for (let m of metrics) {
|
||||
// Get Unit
|
||||
const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit
|
||||
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
|
||||
if (jobs) {
|
||||
let plotIndex = 0
|
||||
jobs.forEach((j) => {
|
||||
// Collect JobIDs & Clusters for X-Ticks and Legend
|
||||
jobIds.push(j.jobId)
|
||||
jobClusters.push(`${j.cluster} ${j.subCluster}`)
|
||||
// Resources
|
||||
comparePlotData['resources'].data[0].push(plotIndex)
|
||||
comparePlotData['resources'].data[1].push(j.startTime)
|
||||
comparePlotData['resources'].data[2].push(j.duration)
|
||||
comparePlotData['resources'].data[3].push(j.numNodes)
|
||||
comparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
|
||||
comparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
|
||||
pendingComparePlotData['resources'].data[0].push(plotIndex)
|
||||
pendingComparePlotData['resources'].data[1].push(j.startTime)
|
||||
pendingComparePlotData['resources'].data[2].push(j.duration)
|
||||
pendingComparePlotData['resources'].data[3].push(j.numNodes)
|
||||
pendingComparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
|
||||
pendingComparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
|
||||
// Metrics
|
||||
for (let s of j.stats) {
|
||||
comparePlotData[s.name].data[0].push(plotIndex)
|
||||
comparePlotData[s.name].data[1].push(j.startTime)
|
||||
comparePlotData[s.name].data[2].push(j.duration)
|
||||
comparePlotData[s.name].data[3].push(s.data.min)
|
||||
comparePlotData[s.name].data[4].push(s.data.avg)
|
||||
comparePlotData[s.name].data[5].push(s.data.max)
|
||||
pendingComparePlotData[s.name].data[0].push(plotIndex)
|
||||
pendingComparePlotData[s.name].data[1].push(j.startTime)
|
||||
pendingComparePlotData[s.name].data[2].push(j.duration)
|
||||
pendingComparePlotData[s.name].data[3].push(s.data.min)
|
||||
pendingComparePlotData[s.name].data[4].push(s.data.avg)
|
||||
pendingComparePlotData[s.name].data[5].push(s.data.max)
|
||||
}
|
||||
plotIndex++
|
||||
})
|
||||
}
|
||||
return {...pendingComparePlotData};
|
||||
}
|
||||
|
||||
// Adapt for Persisting Job Selections in DB later down the line
|
||||
@ -242,7 +239,6 @@
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
</script>
|
||||
|
||||
{#if $compareData.fetching}
|
||||
@ -269,7 +265,7 @@
|
||||
xticks={jobIds}
|
||||
xinfo={jobClusters}
|
||||
ylabel={'Resource Counts'}
|
||||
data={comparePlotData['resources'].data}
|
||||
data={comparePlotData['resources']?.data}
|
||||
{plotSync}
|
||||
forResources
|
||||
/>
|
||||
@ -285,8 +281,8 @@
|
||||
xinfo={jobClusters}
|
||||
ylabel={m}
|
||||
metric={m}
|
||||
yunit={comparePlotData[m].unit}
|
||||
data={comparePlotData[m].data}
|
||||
yunit={comparePlotData[m]?.unit}
|
||||
data={comparePlotData[m]?.data}
|
||||
{plotSync}
|
||||
/>
|
||||
</Col>
|
||||
@ -318,7 +314,7 @@
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</th>
|
||||
<th on:click={() => sortBy('meta', 'startTime')}>
|
||||
<th onclick={() => sortBy('meta', 'startTime')}>
|
||||
Sort
|
||||
<Icon
|
||||
name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime']
|
||||
@ -327,7 +323,7 @@
|
||||
: ''}"
|
||||
/>
|
||||
</th>
|
||||
<th on:click={() => sortBy('meta', 'duration')}>
|
||||
<th onclick={() => sortBy('meta', 'duration')}>
|
||||
Sort
|
||||
<Icon
|
||||
name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration']
|
||||
@ -336,7 +332,7 @@
|
||||
: ''}"
|
||||
/>
|
||||
</th>
|
||||
<th on:click={() => sortBy('meta', 'cluster')}>
|
||||
<th onclick={() => sortBy('meta', 'cluster')}>
|
||||
Sort
|
||||
<Icon
|
||||
name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster']
|
||||
@ -346,7 +342,7 @@
|
||||
/>
|
||||
</th>
|
||||
{#each ["Nodes", "Threads", "Accs"] as res}
|
||||
<th on:click={() => sortBy('resources', res)}>
|
||||
<th onclick={() => sortBy('resources', res)}>
|
||||
{res}
|
||||
<Icon
|
||||
name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res]
|
||||
@ -358,7 +354,7 @@
|
||||
{/each}
|
||||
{#each metrics as metric}
|
||||
{#each ["min", "avg", "max"] as stat}
|
||||
<th on:click={() => sortBy(metric, stat)}>
|
||||
<th onclick={() => sortBy(metric, stat)}>
|
||||
{stat.charAt(0).toUpperCase() + stat.slice(1)}
|
||||
<Icon
|
||||
name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat]
|
||||
|
@ -18,86 +18,30 @@
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
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
|
||||
|
||||
/* 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 lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
|
||||
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
|
||||
|
||||
// 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// UPLOT SERIES INIT //
|
||||
const plotSeries = [
|
||||
{
|
||||
label: "JobID",
|
||||
@ -122,6 +66,7 @@
|
||||
},
|
||||
]
|
||||
|
||||
// UPLOT SCALES INIT //
|
||||
if (forResources) {
|
||||
const resSeries = [
|
||||
{
|
||||
@ -177,11 +122,13 @@
|
||||
plotSeries.push(...statsSeries)
|
||||
};
|
||||
|
||||
// UPLOT BAND COLORS //
|
||||
const plotBands = [
|
||||
{ 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)" },
|
||||
];
|
||||
|
||||
// UPLOT OPTIONS //
|
||||
const opts = {
|
||||
width,
|
||||
height,
|
||||
@ -259,11 +206,83 @@
|
||||
}
|
||||
};
|
||||
|
||||
// RENDER HANDLING
|
||||
let plotWrapper = null;
|
||||
let uplot = null;
|
||||
/* Var Init */
|
||||
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) {
|
||||
if (!uplot) {
|
||||
opts.width = ren_width;
|
||||
@ -284,22 +303,18 @@
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/* 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 -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user