2023-01-27 18:36:58 +01:00
|
|
|
<!--
|
2022-06-22 11:20:57 +02:00
|
|
|
@component
|
|
|
|
|
|
|
|
Only width/height should change reactively.
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
- width: Number
|
|
|
|
- height: Number
|
|
|
|
- timestep: Number
|
|
|
|
- series: [GraphQL.Series]
|
|
|
|
- statisticsSeries: [GraphQL.StatisticsSeries]
|
|
|
|
- cluster: GraphQL.Cluster
|
|
|
|
- subCluster: String
|
|
|
|
- metric: String
|
|
|
|
- scope: String
|
|
|
|
- useStatsSeries: Boolean
|
|
|
|
|
|
|
|
Functions:
|
|
|
|
- setTimeRange(from, to): Void
|
|
|
|
|
|
|
|
// TODO: Move helper functions to module context?
|
|
|
|
-->
|
|
|
|
<script>
|
|
|
|
import uPlot from 'uplot'
|
2023-06-16 12:44:34 +02:00
|
|
|
import { formatNumber } from '../units.js'
|
2022-06-22 11:20:57 +02:00
|
|
|
import { getContext, onMount, onDestroy } from 'svelte'
|
2023-06-16 15:09:23 +02:00
|
|
|
import { Card } from 'sveltestrap'
|
2022-06-22 11:20:57 +02:00
|
|
|
|
|
|
|
export let width
|
|
|
|
export let height
|
|
|
|
export let timestep
|
|
|
|
export let series
|
|
|
|
export let statisticsSeries = null
|
|
|
|
export let cluster
|
|
|
|
export let subCluster
|
|
|
|
export let metric
|
|
|
|
export let useStatsSeries = null
|
|
|
|
export let scope = 'node'
|
2023-04-28 12:30:46 +02:00
|
|
|
export let isShared = false
|
2022-06-22 11:20:57 +02:00
|
|
|
|
|
|
|
if (useStatsSeries == null)
|
|
|
|
useStatsSeries = statisticsSeries != null
|
|
|
|
|
|
|
|
if (useStatsSeries == false && series == null)
|
|
|
|
useStatsSeries = true
|
|
|
|
|
|
|
|
const metricConfig = getContext('metrics')(cluster, metric)
|
|
|
|
const clusterCockpitConfig = getContext('cc-config')
|
|
|
|
const resizeSleepTime = 250
|
|
|
|
const normalLineColor = '#000000'
|
|
|
|
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio
|
|
|
|
const lineColors = clusterCockpitConfig.plot_general_colorscheme
|
|
|
|
const backgroundColors = { normal: 'rgba(255, 255, 255, 1.0)', caution: 'rgba(255, 128, 0, 0.3)', alert: 'rgba(255, 0, 0, 0.3)' }
|
|
|
|
const thresholds = findThresholds(metricConfig, scope, typeof subCluster == 'string' ? cluster.subClusters.find(sc => sc.name == subCluster) : subCluster)
|
|
|
|
|
|
|
|
function backgroundColor() {
|
|
|
|
if (clusterCockpitConfig.plot_general_colorBackground == false
|
|
|
|
|| !thresholds
|
|
|
|
|| !(series && series.every(s => s.statistics != null)))
|
|
|
|
return backgroundColors.normal
|
|
|
|
|
|
|
|
let cond = thresholds.alert < thresholds.caution
|
|
|
|
? (a, b) => a <= b
|
|
|
|
: (a, b) => a >= b
|
|
|
|
|
|
|
|
let avg = series.reduce((sum, series) => sum + series.statistics.avg, 0) / series.length
|
|
|
|
|
|
|
|
if (Number.isNaN(avg))
|
|
|
|
return backgroundColors.normal
|
|
|
|
|
|
|
|
if (cond(avg, thresholds.alert))
|
|
|
|
return backgroundColors.alert
|
|
|
|
|
|
|
|
if (cond(avg, thresholds.caution))
|
|
|
|
return backgroundColors.caution
|
|
|
|
|
|
|
|
return backgroundColors.normal
|
|
|
|
}
|
|
|
|
|
|
|
|
function lineColor(i, n) {
|
|
|
|
if (n >= lineColors.length)
|
|
|
|
return lineColors[i % lineColors.length];
|
|
|
|
else
|
|
|
|
return lineColors[Math.floor((i / n) * lineColors.length)];
|
|
|
|
}
|
|
|
|
|
|
|
|
const longestSeries = useStatsSeries
|
|
|
|
? statisticsSeries.mean.length
|
|
|
|
: series.reduce((n, series) => Math.max(n, series.data.length), 0)
|
|
|
|
const maxX = longestSeries * timestep
|
|
|
|
const maxY = thresholds != null
|
|
|
|
? 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)
|
|
|
|
: null
|
|
|
|
const plotSeries = [{}]
|
|
|
|
const plotData = [new Array(longestSeries)]
|
|
|
|
for (let i = 0; i < longestSeries; i++) // TODO: Cache/Reuse this array?
|
|
|
|
plotData[0][i] = i * timestep
|
|
|
|
|
|
|
|
let plotBands = undefined
|
|
|
|
if (useStatsSeries) {
|
|
|
|
plotData.push(statisticsSeries.min)
|
|
|
|
plotData.push(statisticsSeries.max)
|
|
|
|
plotData.push(statisticsSeries.mean)
|
|
|
|
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'red' })
|
|
|
|
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'green' })
|
|
|
|
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'black' })
|
|
|
|
plotBands = [
|
|
|
|
{ series: [2,3], fill: 'rgba(0,255,0,0.1)' },
|
|
|
|
{ series: [3,1], fill: 'rgba(255,0,0,0.1)' }
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
for (let i = 0; i < series.length; i++) {
|
|
|
|
plotData.push(series[i].data)
|
|
|
|
plotSeries.push({
|
|
|
|
scale: 'y',
|
|
|
|
width: lineWidth,
|
|
|
|
stroke: lineColor(i, series.length)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const opts = {
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
series: plotSeries,
|
|
|
|
axes: [
|
|
|
|
{
|
|
|
|
scale: 'x',
|
|
|
|
space: 35,
|
|
|
|
incrs: timeIncrs(timestep, maxX),
|
|
|
|
values: (_, vals) => vals.map(v => formatTime(v))
|
|
|
|
},
|
|
|
|
{
|
|
|
|
scale: 'y',
|
|
|
|
grid: { show: true },
|
|
|
|
labelFont: 'sans-serif',
|
|
|
|
values: (u, vals) => vals.map(v => formatNumber(v))
|
|
|
|
}
|
|
|
|
],
|
|
|
|
bands: plotBands,
|
|
|
|
padding: [5, 10, -20, 0],
|
|
|
|
hooks: {
|
|
|
|
draw: [(u) => {
|
|
|
|
// Draw plot type label:
|
2023-04-28 12:30:46 +02:00
|
|
|
let textl = `${scope}${plotSeries.length > 2 ? 's' : ''}${
|
2022-07-14 16:25:21 +02:00
|
|
|
useStatsSeries ? ': min/avg/max' : (metricConfig != null && scope != metricConfig.scope ? ` (${metricConfig.aggregation})` : '')}`
|
2023-04-28 12:30:46 +02:00
|
|
|
let textr = `${(isShared && (scope != 'core' && scope != 'accelerator')) ? '[Shared]' : '' }`
|
2022-06-22 11:20:57 +02:00
|
|
|
u.ctx.save()
|
|
|
|
u.ctx.textAlign = 'start' // 'end'
|
|
|
|
u.ctx.fillStyle = 'black'
|
2023-04-28 12:30:46 +02:00
|
|
|
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.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
|
2022-06-22 11:20:57 +02:00
|
|
|
|
|
|
|
if (!thresholds) {
|
|
|
|
u.ctx.restore()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let y = u.valToPos(thresholds.normal, 'y', true)
|
|
|
|
u.ctx.save()
|
|
|
|
u.ctx.lineWidth = lineWidth
|
|
|
|
u.ctx.strokeStyle = normalLineColor
|
|
|
|
u.ctx.setLineDash([5, 5])
|
|
|
|
u.ctx.beginPath()
|
|
|
|
u.ctx.moveTo(u.bbox.left, y)
|
|
|
|
u.ctx.lineTo(u.bbox.left + u.bbox.width, y)
|
|
|
|
u.ctx.stroke()
|
|
|
|
u.ctx.restore()
|
|
|
|
}]
|
|
|
|
},
|
|
|
|
scales: {
|
|
|
|
x: { time: false },
|
|
|
|
y: maxY ? { range: [0., maxY * 1.1] } : {}
|
|
|
|
},
|
2023-07-24 11:12:22 +02:00
|
|
|
cursor: { drag: { x: true, y: true } },
|
2022-06-22 11:20:57 +02:00
|
|
|
legend: { show: false, live: false }
|
|
|
|
}
|
|
|
|
|
|
|
|
// console.log(opts)
|
|
|
|
|
|
|
|
let plotWrapper = null
|
|
|
|
let uplot = null
|
|
|
|
let timeoutId = null
|
|
|
|
let prevWidth = null, prevHeight = null
|
|
|
|
|
|
|
|
function render() {
|
|
|
|
if (!width || Number.isNaN(width) || width < 0)
|
|
|
|
return
|
|
|
|
|
|
|
|
if (prevWidth != null && Math.abs(prevWidth - width) < 10)
|
|
|
|
return
|
|
|
|
|
|
|
|
prevWidth = width
|
|
|
|
prevHeight = height
|
|
|
|
|
|
|
|
if (!uplot) {
|
|
|
|
opts.width = width
|
|
|
|
opts.height = height
|
|
|
|
uplot = new uPlot(opts, plotData, plotWrapper)
|
|
|
|
} else {
|
|
|
|
uplot.setSize({ width, height })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function onSizeChange() {
|
|
|
|
if (!uplot)
|
|
|
|
return
|
|
|
|
|
|
|
|
if (timeoutId != null)
|
|
|
|
clearTimeout(timeoutId)
|
|
|
|
|
|
|
|
timeoutId = setTimeout(() => {
|
|
|
|
timeoutId = null
|
|
|
|
render()
|
|
|
|
}, resizeSleepTime)
|
|
|
|
}
|
|
|
|
|
2023-06-16 15:09:23 +02:00
|
|
|
$: if (series[0].data.length > 0) {
|
|
|
|
onSizeChange(width, height)
|
|
|
|
}
|
2022-06-22 11:20:57 +02:00
|
|
|
|
|
|
|
onMount(() => {
|
2023-06-16 15:09:23 +02:00
|
|
|
if (series[0].data.length > 0) {
|
|
|
|
plotWrapper.style.backgroundColor = backgroundColor()
|
|
|
|
render()
|
|
|
|
}
|
2022-06-22 11:20:57 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
onDestroy(() => {
|
|
|
|
if (uplot)
|
|
|
|
uplot.destroy()
|
|
|
|
|
|
|
|
if (timeoutId != null)
|
|
|
|
clearTimeout(timeoutId)
|
|
|
|
})
|
|
|
|
|
|
|
|
// `from` and `to` must be numbers between 0 and 1.
|
|
|
|
export function setTimeRange(from, to) {
|
|
|
|
if (!uplot || from > to)
|
|
|
|
return false
|
|
|
|
|
|
|
|
uplot.setScale('x', { min: from * maxX, max: to * maxX })
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
<script context="module">
|
|
|
|
|
|
|
|
export function formatTime(t) {
|
|
|
|
let h = Math.floor(t / 3600)
|
|
|
|
let m = Math.floor((t % 3600) / 60)
|
|
|
|
if (h == 0)
|
|
|
|
return `${m}m`
|
|
|
|
else if (m == 0)
|
|
|
|
return `${h}h`
|
|
|
|
else
|
|
|
|
return `${h}:${m}h`
|
|
|
|
}
|
|
|
|
|
|
|
|
export function timeIncrs(timestep, maxX) {
|
|
|
|
let incrs = []
|
|
|
|
for (let t = timestep; t < maxX; t *= 10)
|
|
|
|
incrs.push(t, t * 2, t * 3, t * 5)
|
|
|
|
|
|
|
|
return incrs
|
|
|
|
}
|
|
|
|
|
|
|
|
export function findThresholds(metricConfig, scope, subCluster) {
|
2023-06-20 13:16:00 +02:00
|
|
|
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
|
2023-06-14 16:46:17 +02:00
|
|
|
if (!metricConfig || !scope || !subCluster) {
|
|
|
|
console.warn('Argument missing for findThresholds!')
|
2022-06-22 11:20:57 +02:00
|
|
|
return null
|
2023-06-14 16:46:17 +02:00
|
|
|
}
|
2022-06-22 11:20:57 +02:00
|
|
|
|
|
|
|
if (scope == 'node' || metricConfig.aggregation == 'avg') {
|
2023-06-14 16:46:17 +02:00
|
|
|
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
|
|
|
|
// console.log('subClusterConfigs array empty, use metricConfig defaults')
|
2022-06-22 11:20:57 +02:00
|
|
|
return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert }
|
2023-06-14 16:46:17 +02:00
|
|
|
} else if (metricConfig.subClusters && metricConfig.subClusters.length > 0) {
|
2023-06-20 13:16:00 +02:00
|
|
|
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
|
|
|
|
let forSubCluster = metricConfig.subClusters.find(sc => sc.name == subCluster.name)
|
|
|
|
if (forSubCluster && forSubCluster.normal && forSubCluster.caution && forSubCluster.alert) return forSubCluster
|
|
|
|
else return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert }
|
2023-06-14 16:46:17 +02:00
|
|
|
} else {
|
|
|
|
console.warn('metricConfig.subClusters not found!')
|
|
|
|
return null
|
|
|
|
}
|
2022-06-22 11:20:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (metricConfig.aggregation != 'sum') {
|
|
|
|
console.warn('Missing or unkown aggregation mode (sum/avg) for metric:', metricConfig)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
let divisor = 1
|
|
|
|
if (scope == 'socket')
|
|
|
|
divisor = subCluster.topology.socket.length
|
|
|
|
else if (scope == 'core')
|
|
|
|
divisor = subCluster.topology.core.length
|
|
|
|
else if (scope == 'accelerator')
|
|
|
|
divisor = subCluster.topology.accelerators.length
|
|
|
|
else if (scope == 'hwthread')
|
|
|
|
divisor = subCluster.topology.node.length
|
|
|
|
else {
|
2023-01-27 18:36:58 +01:00
|
|
|
// console.log('TODO: how to calc thresholds for ', scope)
|
2022-06-22 11:20:57 +02:00
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
let mc = metricConfig?.subClusters?.find(sc => sc.name == subCluster.name) || metricConfig
|
|
|
|
return {
|
|
|
|
normal: mc.normal / divisor,
|
|
|
|
caution: mc.caution / divisor,
|
|
|
|
alert: mc.alert / divisor
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
2023-06-16 15:09:23 +02:00
|
|
|
{#if series[0].data.length > 0}
|
|
|
|
<div bind:this={plotWrapper} class="cc-plot"></div>
|
|
|
|
{:else}
|
2023-06-19 16:11:16 +02:00
|
|
|
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">Cannot render plot: No series data returned for <code>{metric}</code></Card>
|
2023-06-16 15:09:23 +02:00
|
|
|
{/if}
|
2022-06-22 11:20:57 +02:00
|
|
|
<style>
|
|
|
|
.cc-plot {
|
|
|
|
border-radius: 5px;
|
|
|
|
}
|
|
|
|
</style>
|