mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-10-31 16:05:06 +01:00 
			
		
		
		
	initial branch commit, improve countstate backend logic
- stacked component rough sketch - gql data request pipeline layed out
This commit is contained in:
		| @@ -20,7 +20,7 @@ | ||||
|     // mutationStore, | ||||
|   } from "@urql/svelte"; | ||||
|   import { Row, Col, Card, Spinner, Table, Input, InputGroup, InputGroupText, Icon } from "@sveltestrap/sveltestrap"; | ||||
|   import { formatTime, roundTwoDigits } from "./units.js"; | ||||
|   import { formatDurationTime, roundTwoDigits } from "./units.js"; | ||||
|   import Comparogram from "./plots/Comparogram.svelte"; | ||||
|  | ||||
|   /* Svelte 5 Props */ | ||||
| @@ -373,7 +373,7 @@ | ||||
|           <tr> | ||||
|             <td><b><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a></b></td> | ||||
|             <td>{new Date(job.startTime * 1000).toLocaleString()}</td> | ||||
|             <td>{formatTime(job.duration)}</td> | ||||
|             <td>{formatDurationTime(job.duration)}</td> | ||||
|             <td>{job.cluster} ({job.subCluster})</td> | ||||
|             <td>{job.numNodes}</td> | ||||
|             <td>{job.numHWThreads}</td> | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  | ||||
| <script> | ||||
|   import uPlot from "uplot"; | ||||
|   import { roundTwoDigits, formatTime, formatNumber } from "../units.js"; | ||||
|   import { roundTwoDigits, formatDurationTime, formatNumber } from "../units.js"; | ||||
|   import { getContext, onMount, onDestroy } from "svelte"; | ||||
|   import { Card } from "@sveltestrap/sveltestrap"; | ||||
|  | ||||
| @@ -67,7 +67,7 @@ | ||||
|       label: "Duration", | ||||
|       scale: "xrt", | ||||
|       value: (u, ts, sidx, didx) => { | ||||
|         return formatTime(ts); | ||||
|         return formatDurationTime(ts); | ||||
|       }, | ||||
|     }, | ||||
|   ] | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
| <script> | ||||
|   import uPlot from "uplot"; | ||||
|   import { onMount, onDestroy } from "svelte"; | ||||
|   import { formatNumber, formatTime } from "../units.js"; | ||||
|   import { formatNumber, formatDurationTime } from "../units.js"; | ||||
|   import { Card } from "@sveltestrap/sveltestrap"; | ||||
|  | ||||
|   /* Svelte 5 Props */ | ||||
| @@ -180,7 +180,7 @@ | ||||
|           }, | ||||
|           values: (_, t) => t.map((v) => { | ||||
|             if (xtime) { | ||||
|               return formatTime(v); | ||||
|               return formatDurationTime(v); | ||||
|             } else { | ||||
|               return formatNumber(v) | ||||
|             } | ||||
| @@ -211,15 +211,15 @@ | ||||
|           label: xunit !== "" ? xunit : null, | ||||
|           value: (u, ts, sidx, didx) => { | ||||
|             if (usesBins && xtime) { | ||||
|               const min = u.data[sidx][didx - 1] ? formatTime(u.data[sidx][didx - 1]) : 0; | ||||
|               const max = formatTime(u.data[sidx][didx]); | ||||
|               const min = u.data[sidx][didx - 1] ? formatDurationTime(u.data[sidx][didx - 1]) : 0; | ||||
|               const max = formatDurationTime(u.data[sidx][didx]); | ||||
|               ts = min + " - " + max; // narrow spaces | ||||
|             } else if (usesBins) { | ||||
|               const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0; | ||||
|               const max = u.data[sidx][didx]; | ||||
|               ts = min + " - " + max; // narrow spaces | ||||
|             } else if (xtime) { | ||||
|               ts = formatTime(ts); | ||||
|               ts = formatDurationTime(ts); | ||||
|             } | ||||
|             return ts; | ||||
|           }, | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
|  | ||||
| <script> | ||||
|   import uPlot from "uplot"; | ||||
|   import { formatNumber, formatTime } from "../units.js"; | ||||
|   import { formatNumber, formatDurationTime } from "../units.js"; | ||||
|   import { getContext, onMount, onDestroy } from "svelte"; | ||||
|   import { Card } from "@sveltestrap/sveltestrap"; | ||||
|  | ||||
| @@ -162,7 +162,7 @@ | ||||
|       { | ||||
|         label: "Runtime", | ||||
|         value: (u, ts, sidx, didx) => | ||||
|         (didx == null) ? null : formatTime(ts, forNode), | ||||
|         (didx == null) ? null : formatDurationTime(ts, forNode), | ||||
|       } | ||||
|     ]; | ||||
|     // Y | ||||
| @@ -226,14 +226,14 @@ | ||||
|  | ||||
|               if (series[i].id in extendedLegendData) { | ||||
|                 return { | ||||
|                   time: formatTime(plotData[0][idx], forNode), | ||||
|                   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: formatTime(plotData[0][idx], forNode), | ||||
|                   time: formatDurationTime(plotData[0][idx], forNode), | ||||
|                   value: plotData[sidx][idx], | ||||
|                   user: '-', | ||||
|                   job: '-', | ||||
| @@ -457,7 +457,7 @@ | ||||
|           scale: "x", | ||||
|           space: 35, | ||||
|           incrs: timeIncrs(timestep, maxX, forNode), | ||||
|           values: (_, vals) => vals.map((v) => formatTime(v, forNode)), | ||||
|           values: (_, vals) => vals.map((v) => formatDurationTime(v, forNode)), | ||||
|         }, | ||||
|         { | ||||
|           scale: "y", | ||||
|   | ||||
							
								
								
									
										570
									
								
								web/frontend/src/generic/plots/Stacked.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										570
									
								
								web/frontend/src/generic/plots/Stacked.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,570 @@ | ||||
| <!-- | ||||
|   @component Node State/Health Data Stacked Plot Component, based on uPlot; states by timestamp | ||||
|  | ||||
|   Only width/height should change reactively. | ||||
|  | ||||
|   Properties: | ||||
|   - `metric String?`: The metric name [Default: ""] | ||||
|   - `width Number?`: The plot width [Default: 0] | ||||
|   - `height Number?`: The plot height [Default: 300] | ||||
|   - `data [Array]`: The data object [Default: null] | ||||
|   - `title String?`: Plot title [Default: ""] | ||||
|   - `xlabel String?`: Plot X axis label [Default: ""] | ||||
|   - `ylabel String?`: Plot Y axis label [Default: ""] | ||||
|   - `yunit String?`: Plot Y axis unit [Default: ""] | ||||
|   - `xticks Array`: Array containing jobIDs [Default: []] | ||||
|   - `xinfo Array`: Array containing job information [Default: []] | ||||
|   - `forResources Bool?`: Render this plot for allocated jobResources [Default: false] | ||||
|   - `plot Sync Object!`: uPlot cursor synchronization key | ||||
| --> | ||||
|  | ||||
| <script> | ||||
|   import uPlot from "uplot"; | ||||
|   import { roundTwoDigits, formatDurationTime, formatUnixTime, formatNumber } from "../units.js"; | ||||
|   import { getContext, onMount, onDestroy } from "svelte"; | ||||
|   import { Card } from "@sveltestrap/sveltestrap"; | ||||
|  | ||||
|   // NOTE: Metric Thresholds non-required, Cluster Mixing Allowed | ||||
|  | ||||
|   /* Svelte 5 Props */ | ||||
|   let { | ||||
|     cluster = "", | ||||
|     width = 0, | ||||
|     height = 300, | ||||
|     data = null, | ||||
|     xlabel = "", | ||||
|     ylabel = "", | ||||
|     yunit = "", | ||||
|     title = "", | ||||
|     stateType = "" // Health, Slurm, Both | ||||
|   } = $props(); | ||||
|  | ||||
|   /* Const Init */ | ||||
|   const clusterCockpitConfig = getContext("cc-config"); | ||||
|   const lineWidth = clusterCockpitConfig?.plotConfiguration_lineWidth / window.devicePixelRatio || 2; | ||||
|   const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false; | ||||
|  | ||||
|   // STACKED CHART FUNCTIONS // | ||||
|  | ||||
|   function stack(data, omit) { | ||||
|     let data2 = []; | ||||
|     let bands = []; | ||||
|     let d0Len = data[0].length; | ||||
|     let accum = Array(d0Len); | ||||
|  | ||||
|     for (let i = 0; i < d0Len; i++) | ||||
|       accum[i] = 0; | ||||
|  | ||||
|     for (let i = 1; i < data.length; i++) | ||||
|       data2.push(omit(i) ? data[i] : data[i].map((v, i) => (accum[i] += +v))); | ||||
|  | ||||
|     for (let i = 1; i < data.length; i++) | ||||
|       !omit(i) && bands.push({ | ||||
|         series: [ | ||||
|           data.findIndex((s, j) => j > i && !omit(j)), | ||||
|           i, | ||||
|         ], | ||||
|       }); | ||||
|  | ||||
|     bands = bands.filter(b => b.series[1] > -1); | ||||
|  | ||||
|     return { | ||||
|       data: [data[0]].concat(data2), | ||||
|       bands, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   function getOpts(title, series) { | ||||
|     return { | ||||
|       scales: { | ||||
|         x: { | ||||
|           time: false, | ||||
|         }, | ||||
|       }, | ||||
|       series | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   function getStackedOpts(title, series, data, interp) { | ||||
|     let opts = getOpts(title, series); | ||||
|  | ||||
|     let interped = interp ? interp(data) : data; | ||||
|  | ||||
|     let stacked = stack(interped, i => false); | ||||
|     opts.bands = stacked.bands; | ||||
|  | ||||
|     opts.cursor = opts.cursor || {}; | ||||
|     opts.cursor.dataIdx = (u, seriesIdx, closestIdx, xValue) => { | ||||
|       return data[seriesIdx][closestIdx] == null ? null : closestIdx; | ||||
|     }; | ||||
|  | ||||
|     opts.series.forEach(s => { | ||||
|       s.value = (u, v, si, i) => data[si][i]; | ||||
|  | ||||
|       s.points = s.points || {}; | ||||
|  | ||||
|       // scan raw unstacked data to return only real points | ||||
|       s.points.filter = (u, seriesIdx, show, gaps) => { | ||||
|         if (show) { | ||||
|           let pts = []; | ||||
|           data[seriesIdx].forEach((v, i) => { | ||||
|             v != null && pts.push(i); | ||||
|           }); | ||||
|           return pts; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // force 0 to be the sum minimum this instead of the bottom series | ||||
|     opts.scales.y = { | ||||
|       range: (u, min, max) => { | ||||
|         let minMax = uPlot.rangeNum(0, max, 0.1, true); | ||||
|         return [0, minMax[1]]; | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     // restack on toggle | ||||
|     opts.hooks = { | ||||
|       setSeries: [ | ||||
|         (u, i) => { | ||||
|           let stacked = stack(data, i => !u.series[i].show); | ||||
|           u.delBand(null); | ||||
|           stacked.bands.forEach(b => u.addBand(b)); | ||||
|           u.setData(stacked.data); | ||||
|         } | ||||
|       ], | ||||
|     }; | ||||
|  | ||||
|     return {opts, data: stacked.data}; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   function stack2(series) { | ||||
|     // for uplot data | ||||
|     let data = Array(series.length); | ||||
|     let bands = []; | ||||
|  | ||||
|     let dataLen = series[0].values.length; | ||||
|  | ||||
|     let zeroArr = Array(dataLen).fill(0); | ||||
|  | ||||
|     let stackGroups = new Map(); | ||||
|     let seriesStackKeys = Array(series.length); | ||||
|  | ||||
|     series.forEach((s, si) => { | ||||
|       let vals = s.values.slice(); | ||||
|  | ||||
|       // apply negY | ||||
|       if (s.negY) { | ||||
|         for (let i = 0; i < vals.length; i++) { | ||||
|           if (vals[i] != null) | ||||
|             vals[i] *= -1; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (s.stacking.mode != 'none') { | ||||
|         let hasPos = vals.some(v => v > 0); | ||||
|         // derive stacking key | ||||
|         let stackKey = seriesStackKeys[si] = s.stacking.mode + s.scaleKey + s.stacking.group + (hasPos ? '+' : '-'); | ||||
|         let group = stackGroups.get(stackKey); | ||||
|  | ||||
|         // initialize stacking group | ||||
|         if (group == null) { | ||||
|           group = { | ||||
|             series: [], | ||||
|             acc: zeroArr.slice(), | ||||
|             dir: hasPos ? -1 : 1, | ||||
|           }; | ||||
|           stackGroups.set(stackKey, group); | ||||
|         } | ||||
|  | ||||
|         // push for bands gen | ||||
|         group.series.unshift(si); | ||||
|  | ||||
|         let stacked = data[si] = Array(dataLen); | ||||
|         let { acc } = group; | ||||
|  | ||||
|         for (let i = 0; i < dataLen; i++) { | ||||
|           let v = vals[i]; | ||||
|  | ||||
|           if (v != null) | ||||
|             stacked[i] = (acc[i] += v); | ||||
|           else | ||||
|             stacked[i] = v; // we may want to coerce to 0 here | ||||
|         } | ||||
|       } | ||||
|       else | ||||
|         data[si] = vals; | ||||
|     }); | ||||
|  | ||||
|     // re-compute by percent | ||||
|     series.forEach((s, si) => { | ||||
|       if (s.stacking.mode == 'percent') { | ||||
|         let group = stackGroups.get(seriesStackKeys[si]); | ||||
|         let { acc } = group; | ||||
|  | ||||
|         // re-negatify percent | ||||
|         let sign = group.dir * -1; | ||||
|  | ||||
|         let stacked = data[si]; | ||||
|  | ||||
|         for (let i = 0; i < dataLen; i++) { | ||||
|           let v = stacked[i]; | ||||
|  | ||||
|           if (v != null) | ||||
|             stacked[i] = sign * (v / acc[i]); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // generate bands between adjacent group series | ||||
|     stackGroups.forEach(group => { | ||||
|       let { series, dir } = group; | ||||
|       let lastIdx = series.length - 1; | ||||
|  | ||||
|       series.forEach((si, i) => { | ||||
|         if (i != lastIdx) { | ||||
|           let nextIdx = series[i + 1]; | ||||
|           bands.push({ | ||||
|             // since we're not passing x series[0] for stacking, real idxs are actually +1 | ||||
|             series: [si + 1, nextIdx + 1], | ||||
|             dir, | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     return { | ||||
|       data, | ||||
|       bands, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // UPLOT SERIES INIT // | ||||
|  | ||||
|   const plotSeries = [ | ||||
|       { | ||||
|         label: "Time", | ||||
|         scale: "x", | ||||
|         value: (u, ts, sidx, didx) => | ||||
|         (didx == null) ? null : formatUnixTime(ts), | ||||
|       } | ||||
|   ] | ||||
|  | ||||
|   if (stateType === "slurm") { | ||||
|     const resSeries = [ | ||||
|       { | ||||
|         label: "Idle", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(136, 204, 238)" : "lightblue", | ||||
|       }, | ||||
|       { | ||||
|         label: "Allocated", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(30, 136, 229)" : "green", | ||||
|       }, | ||||
|       { | ||||
|         label: "Reserved", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(211, 95, 183)" : "magenta", | ||||
|       }, | ||||
|       { | ||||
|         label: "Mixed", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(239, 230, 69)" : "yellow", | ||||
|       }, | ||||
|       { | ||||
|         label: "Down", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(225, 86, 44)" : "red", | ||||
|       }, | ||||
|       { | ||||
|         label: "Unknown", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: "black", | ||||
|       } | ||||
|     ]; | ||||
|     plotSeries.push(...resSeries) | ||||
|   } else if (stateType === "health") { | ||||
|     const resSeries = [ | ||||
|       { | ||||
|         label: "Full", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(30, 136, 229)" : "green", | ||||
|       }, | ||||
|       { | ||||
|         label: "Partial", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(239, 230, 69)" : "yellow", | ||||
|       }, | ||||
|       { | ||||
|         label: "Failed", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(225, 86, 44)" : "red", | ||||
|       } | ||||
|     ]; | ||||
|     plotSeries.push(...resSeries) | ||||
|   } else { | ||||
|     const resSeries = [ | ||||
|       { | ||||
|         label: "Full", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(30, 136, 229)" : "green", | ||||
|       }, | ||||
|       { | ||||
|         label: "Partial", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(239, 230, 69)" : "yellow", | ||||
|       }, | ||||
|       { | ||||
|         label: "Failed", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(225, 86, 44)" : "red", | ||||
|       }, | ||||
|       { | ||||
|         label: "Idle", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(136, 204, 238)" : "lightblue", | ||||
|       }, | ||||
|       { | ||||
|         label: "Allocated", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(30, 136, 229)" : "green", | ||||
|       }, | ||||
|       { | ||||
|         label: "Reserved", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(211, 95, 183)" : "magenta", | ||||
|       }, | ||||
|       { | ||||
|         label: "Mixed", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(239, 230, 69)" : "yellow", | ||||
|       }, | ||||
|       { | ||||
|         label: "Down", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: cbmode ? "rgb(225, 86, 44)" : "red", | ||||
|       }, | ||||
|       { | ||||
|         label: "Unknown", | ||||
|         scale: "y", | ||||
|         width: lineWidth, | ||||
|         stroke: "black", | ||||
|       } | ||||
|     ]; | ||||
|     plotSeries.push(...resSeries) | ||||
|   } | ||||
|  | ||||
|   // 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, | ||||
|     title, | ||||
|     plugins: [legendAsTooltipPlugin()], | ||||
|     series: plotSeries, | ||||
|     axes: [ | ||||
|       { | ||||
|         scale: "x", | ||||
|         space: 25, // Tick Spacing | ||||
|         rotate: 30, | ||||
|         show: true, | ||||
|         label: xlabel, | ||||
|         // values(self, splits) { | ||||
|         //   return splits.map(s => xticks[s]); | ||||
|         // } | ||||
|       }, | ||||
|       { | ||||
|         scale: "y", | ||||
|         grid: { show: true }, | ||||
|         labelFont: "sans-serif", | ||||
|         label: ylabel + (yunit ? ` (${yunit})` : ''), | ||||
|         // values: (u, vals) => vals.map((v) => formatNumber(v)), | ||||
|       }, | ||||
|     ], | ||||
|     // bands: forResources ? [] : plotBands, | ||||
|     padding: [5, 10, 0, 0], | ||||
|     // hooks: { | ||||
|     //   draw: [ | ||||
|     //     (u) => { | ||||
|     //       // Draw plot type label: | ||||
|     //       let textl = forResources ? "Job Resources by Type" : "Metric Min/Avg/Max for Job Duration"; | ||||
|     //       let textr = "Earlier <- StartTime -> Later"; | ||||
|     //       u.ctx.save(); | ||||
|     //       u.ctx.textAlign = "start"; | ||||
|     //       u.ctx.fillStyle = "black"; | ||||
|     //       u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10); | ||||
|     //       u.ctx.textAlign = "end"; | ||||
|     //       u.ctx.fillStyle = "black"; | ||||
|     //       u.ctx.fillText( | ||||
|     //         textr, | ||||
|     //         u.bbox.left + u.bbox.width - 10, | ||||
|     //         u.bbox.top + 10, | ||||
|     //       ); | ||||
|     //       u.ctx.restore(); | ||||
|     //       return; | ||||
|     //     }, | ||||
|     //   ] | ||||
|     // }, | ||||
|     scales: { | ||||
|       x: { time: false }, | ||||
|       y: {auto: true, distr: 1}, | ||||
|     }, | ||||
|     legend: { | ||||
|       // Display legend | ||||
|       show: true, | ||||
|       live: true, | ||||
|     }, | ||||
|     cursor: {  | ||||
|       drag: { x: true, y: true }, | ||||
|       // sync: {  | ||||
|       //   key: plotSync.key, | ||||
|       //   scales: ["x", 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; | ||||
|       opts.height = ren_height; | ||||
|       uplot = new uPlot(opts, data, plotWrapper); // Data is uplot formatted [[X][Ymin][Yavg][Ymax]] | ||||
|       plotSync.sub(uplot) | ||||
|     } else { | ||||
|       uplot.setSize({ width: ren_width, height: ren_height }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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 */ | ||||
|   onDestroy(() => { | ||||
|     if (timeoutId != null) clearTimeout(timeoutId); | ||||
|     if (uplot) uplot.destroy(); | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <!-- Define $width Wrapper and NoData Card --> | ||||
| {#if data && data[0].length > 0} | ||||
|   <div bind:this={plotWrapper} bind:clientWidth={width} | ||||
|         style="background-color: rgba(255, 255, 255, 1.0);" class="rounded" | ||||
|   ></div> | ||||
| {:else} | ||||
|   <Card body color="warning" class="mx-4 my-2" | ||||
|     >Cannot render plot: No series data returned for <code>{metric?metric:'job resources'}</code></Card | ||||
|   > | ||||
| {/if} | ||||
| @@ -35,7 +35,7 @@ export function scaleNumbers(x, y , p = '') { | ||||
|     return Math.abs(rawYValue) >= 1000 ? `${rawXValue.toExponential()} / ${rawYValue.toExponential()}` : `${rawYValue.toString()} / ${rawYValue.toString()}` | ||||
| } | ||||
|  | ||||
| export function formatTime(t, forNode = false) { | ||||
| export function formatDurationTime(t, forNode = false) { | ||||
|     if (t !== null) { | ||||
|         if (isNaN(t)) { | ||||
|             return t; | ||||
| @@ -51,6 +51,16 @@ export function formatTime(t, forNode = false) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function formatUnixTime(t) { | ||||
|     if (t !== null) { | ||||
|         if (isNaN(t)) { | ||||
|             return t; | ||||
|         } else { | ||||
|             return new Date(t * 1000).toLocaleString() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // const equalsCheck = (a, b) => { | ||||
| //   return JSON.stringify(a) === JSON.stringify(b); | ||||
| // } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
|   import { | ||||
|     init, | ||||
|   } from "../generic/utils.js"; | ||||
|   import { scaleNumbers, formatTime } from "../generic/units.js"; | ||||
|   import { scaleNumbers, formatDurationTime } from "../generic/units.js"; | ||||
|   import Refresher from "../generic/helper/Refresher.svelte"; | ||||
|   import Roofline from "../generic/plots/Roofline.svelte"; | ||||
|   import Pie, { colors } from "../generic/plots/Pie.svelte"; | ||||
| @@ -44,6 +44,7 @@ | ||||
|   /* State Init */ | ||||
|   let cluster = $state(presetCluster); | ||||
|   let pieWidth = $state(0); | ||||
|   let stackedWidth = $state(0); | ||||
|   let plotWidths = $state([]); | ||||
|   let from = $state(new Date(Date.now() - 5 * 60 * 1000)); | ||||
|   let to = $state(new Date(Date.now())); | ||||
| @@ -86,6 +87,24 @@ | ||||
|     return $nodesStateCounts?.data?.nodeStates.filter((e) => ['full', 'partial', 'failed'].includes(e.state)) | ||||
|   }); | ||||
|  | ||||
|   // NodeStates for Stacked charts | ||||
|   const nodesStateTimes = $derived(queryStore({ | ||||
|     client: client, | ||||
|     query: gql` | ||||
|       query ($filter: [NodeFilter!]) { | ||||
|         nodeStatesTimed(filter: $filter) { | ||||
|           state | ||||
|           type | ||||
|           count | ||||
|           time | ||||
|         } | ||||
|       } | ||||
|     `, | ||||
|     variables: { | ||||
|       filter: { cluster: { eq: cluster }, timeStart: Date.now() - (24 * 3600 * 1000)} // Add Selector for Timeframe (4h, 12h, 24h)? | ||||
|     }, | ||||
|   })); | ||||
|  | ||||
|   // Note: nodeMetrics are requested on configured $timestep resolution | ||||
|   // Result: The latest 5 minutes (datapoints) for each node independent of job | ||||
|   const statusQuery = $derived(queryStore({ | ||||
| @@ -315,7 +334,7 @@ | ||||
|  | ||||
|   function transformJobsStatsToInfo(subclusterData) { | ||||
|     if (subclusterData) { | ||||
|         return subclusterData.map((sc) => { return {id: sc.id, jobId: sc.jobId, numNodes: sc.numNodes, numAcc: sc?.numAccelerators? sc.numAccelerators : 0, duration: formatTime(sc.duration)} }) | ||||
|         return subclusterData.map((sc) => { return {id: sc.id, jobId: sc.jobId, numNodes: sc.numNodes, numAcc: sc?.numAccelerators? sc.numAccelerators : 0, duration: formatDurationTime(sc.duration)} }) | ||||
|     } else { | ||||
|         console.warn("transformJobsStatsToInfo: jobInfo missing!") | ||||
|         return [] | ||||
| @@ -374,6 +393,55 @@ | ||||
|  | ||||
| <hr/> | ||||
|  | ||||
| <!-- Node Stack Charts Dev--> | ||||
| <!-- | ||||
| {#if $initq.data && $nodesStateTimes.data} | ||||
|   <Row cols={{ lg: 4, md: 2 , sm: 1}} class="mb-3 justify-content-center"> | ||||
|     <Col class="px-3 mt-2 mt-lg-0"> | ||||
|       <div bind:clientWidth={stackedWidth}> | ||||
|         {#key $nodesStateTimes.data} | ||||
|           <h4 class="text-center"> | ||||
|             {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States Over Time | ||||
|           </h4> | ||||
|           <Stacked | ||||
|             {cluster} | ||||
|             data={$nodesStateTimes?.data} | ||||
|             width={stackedWidth * 0.55} | ||||
|             xLabel="Time" | ||||
|             yLabel="Nodes" | ||||
|             yunit = "#Count" | ||||
|             title = "Slurm States" | ||||
|             stateType = "slurm" | ||||
|           /> | ||||
|         {/key} | ||||
|       </div> | ||||
|     </Col> | ||||
|     <Col class="px-3 mt-2 mt-lg-0"> | ||||
|       <div bind:clientWidth={stackedWidth}> | ||||
|         {#key $nodesStateTimes.data} | ||||
|           <h4 class="text-center"> | ||||
|             {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Health States Over Time | ||||
|           </h4> | ||||
|           <Stacked | ||||
|             {cluster} | ||||
|             data={$nodesStateTimes?.data} | ||||
|             width={stackedWidth * 0.55} | ||||
|             xLabel="Time" | ||||
|             yLabel="Nodes" | ||||
|             yunit = "#Count" | ||||
|             title = "Health States" | ||||
|             stateType = "health" | ||||
|           /> | ||||
|         {/key} | ||||
|       </div> | ||||
|     </Col> | ||||
|   </Row> | ||||
| {/if} | ||||
|  | ||||
| <hr/> | ||||
| <hr/> | ||||
| --> | ||||
|  | ||||
| <!-- Node Health Pis, later Charts --> | ||||
| {#if $initq.data && $nodesStateCounts.data} | ||||
|   <Row cols={{ lg: 4, md: 2 , sm: 1}} class="mb-3 justify-content-center"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user