diff --git a/web/frontend/src/generic/JobCompare.svelte b/web/frontend/src/generic/JobCompare.svelte index 2920eea..c5498f9 100644 --- a/web/frontend/src/generic/JobCompare.svelte +++ b/web/frontend/src/generic/JobCompare.svelte @@ -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 @@ // } // }); // } - {#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} /> @@ -318,7 +314,7 @@ - sortBy('meta', 'startTime')}> + sortBy('meta', 'startTime')}> Sort - sortBy('meta', 'duration')}> + sortBy('meta', 'duration')}> Sort - sortBy('meta', 'cluster')}> + sortBy('meta', 'cluster')}> Sort sortBy('resources', res)}> + sortBy('resources', res)}> {res} sortBy(metric, stat)}> + sortBy(metric, stat)}> {stat.charAt(0).toUpperCase() + stat.slice(1)} { - 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); - } -