Import svelte web frontend

This commit is contained in:
Jan Eitzinger
2022-06-22 11:20:57 +02:00
parent 9217780760
commit 68d1f5fc3f
60 changed files with 6661 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
<!--
@component
Properties:
- width, height: Number
- min, max: Number
- label: (x-Value) => String
- data: [{ value: Number, count: Number }]
-->
<div
on:mousemove={mousemove}
on:mouseleave={() => (infoText = '')}>
<span style="left: {paddingLeft + 5}px;">{infoText}</span>
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
</div>
<script>
import { onMount } from 'svelte'
export let data
export let width
export let height
export let min = null
export let max = null
export let label = formatNumber
const fontSize = 12
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
const paddingLeft = 35, paddingRight = 20, paddingTop = 20, paddingBottom = 20
let ctx, canvasElement
const maxCount = data.reduce((max, point) => Math.max(max, point.count), 0),
maxValue = data.reduce((max, point) => Math.max(max, point.value), 0.1)
function getStepSize(valueRange, pixelRange, minSpace) {
const proposition = valueRange / (pixelRange / minSpace)
const getStepSize = n => Math.pow(10, Math.floor(n / 3)) *
(n < 0 ? [1., 5., 2.][-n % 3] : [1., 2., 5.][n % 3])
let n = 0
let stepsize = getStepSize(n)
while (true) {
let bigger = getStepSize(n + 1)
if (proposition > bigger) {
n += 1
stepsize = bigger
} else {
return stepsize
}
}
}
let infoText = ''
function mousemove(event) {
let rect = event.target.getBoundingClientRect()
let x = event.clientX - rect.left
if (x < paddingLeft || x > width - paddingRight) {
infoText = ''
return
}
const w = width - paddingLeft - paddingRight
const barWidth = Math.round(w / (maxValue + 1))
x = Math.floor((x - paddingLeft) / (w - barWidth) * maxValue)
let point = data.find(point => point.value == x)
if (point)
infoText = `count: ${point.count} (value: ${label(x)})`
else
infoText = ''
}
function render() {
const h = height - paddingTop - paddingBottom
const w = width - paddingLeft - paddingRight
const barWidth = Math.ceil(w / (maxValue + 1))
if (Number.isNaN(barWidth))
return
const getCanvasX = (value) => (value / maxValue) * (w - barWidth) + paddingLeft + (barWidth / 2.)
const getCanvasY = (count) => (h - (count / maxCount) * h) + paddingTop
// X Axis
ctx.font = `${fontSize}px ${fontFamily}`
ctx.fillStyle = 'black'
ctx.textAlign = 'center'
if (min != null && max != null) {
const stepsizeX = getStepSize(max - min, w, 75)
let startX = 0
while (startX < min)
startX += stepsizeX
for (let x = startX; x < max; x += stepsizeX) {
let px = ((x - min) / (max - min)) * (w - barWidth) + paddingLeft + (barWidth / 2.)
ctx.fillText(`${formatNumber(x)}`, px, height - paddingBottom + 15)
}
} else {
const stepsizeX = getStepSize(maxValue, w, 120)
for (let x = 0; x <= maxValue; x += stepsizeX) {
ctx.fillText(label(x), getCanvasX(x), height - paddingBottom + 15)
}
}
// Y Axis
ctx.fillStyle = 'black'
ctx.strokeStyle = '#bbbbbb'
ctx.textAlign = 'right'
ctx.beginPath()
const stepsizeY = getStepSize(maxCount, h, 50)
for (let y = stepsizeY; y <= maxCount; y += stepsizeY) {
const py = Math.floor(getCanvasY(y))
ctx.fillText(`${formatNumber(y)}`, paddingLeft - 5, py)
ctx.moveTo(paddingLeft, py)
ctx.lineTo(width, py)
}
ctx.stroke()
// Draw bars
ctx.fillStyle = '#0066cc'
for (let p of data) {
ctx.fillRect(
getCanvasX(p.value) - (barWidth / 2.),
getCanvasY(p.count),
barWidth,
(p.count / maxCount) * h)
}
// Fat lines left and below plotting area
ctx.strokeStyle = 'black'
ctx.beginPath()
ctx.moveTo(0, height - paddingBottom)
ctx.lineTo(width, height - paddingBottom)
ctx.moveTo(paddingLeft, 0)
ctx.lineTo(paddingLeft, height- paddingBottom)
ctx.stroke()
}
let mounted = false
onMount(() => {
mounted = true
canvasElement.width = width
canvasElement.height = height
ctx = canvasElement.getContext('2d')
render()
})
let timeoutId = null;
function sizeChanged() {
if (timeoutId != null)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
timeoutId = null
if (!canvasElement)
return
canvasElement.width = width
canvasElement.height = height
ctx = canvasElement.getContext('2d')
render()
}, 250)
}
$: sizeChanged(width, height)
</script>
<style>
div {
position: relative;
}
div > span {
position: absolute;
top: 0px;
}
</style>
<script context="module">
import { formatNumber } from '../utils.js'
export function binsFromFootprint(weights, values, numBins) {
let min = 0, max = 0
if (values.length != 0) {
for (let x of values) {
min = Math.min(min, x)
max = Math.max(max, x)
}
max += 1 // So that we have an exclusive range.
}
if (numBins == null || numBins < 3)
numBins = 3
const bins = new Array(numBins).fill(0)
for (let i = 0; i < values.length; i++)
bins[Math.floor(((values[i] - min) / (max - min)) * numBins)] += weights ? weights[i] : 1
return {
label: idx => {
let start = min + (idx / numBins) * (max - min)
let stop = min + ((idx + 1) / numBins) * (max - min)
return `${formatNumber(start)} - ${formatNumber(stop)}`
},
bins: bins.map((count, idx) => ({ value: idx, count: count })),
min: min,
max: max
}
}
</script>

View File

@@ -0,0 +1,306 @@
<!--
@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'
import { formatNumber } from '../utils.js'
import { getContext, onMount, onDestroy } from 'svelte'
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'
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:
let text = `${scope}${plotSeries.length > 2 ? 's' : ''}${useStatsSeries ? ': min/avg/max' : ''}`
u.ctx.save()
u.ctx.textAlign = 'start' // 'end'
u.ctx.fillStyle = 'black'
u.ctx.fillText(text, u.bbox.left + 10, u.bbox.top + 10)
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10)
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] } : {}
},
cursor: { show: false },
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)
}
$: onSizeChange(width, height)
onMount(() => {
plotWrapper.style.backgroundColor = backgroundColor()
render()
})
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) {
if (!metricConfig || !scope || !subCluster)
return null
if (scope == 'node' || metricConfig.aggregation == 'avg') {
if (!metricConfig.subClusters)
return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert }
else
return metricConfig.subClusters.find(sc => sc.name == subCluster.name)
}
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 {
console.log('TODO: how to calc thresholds for ', scope)
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>
<div bind:this={plotWrapper} class="cc-plot"></div>
<style>
.cc-plot {
border-radius: 5px;
}
</style>

View File

@@ -0,0 +1,190 @@
<div>
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
</div>
<script>
import { onMount, getContext } from 'svelte'
export let metrics
export let width
export let height
export let cluster
export let jobMetrics
const fontSize = 12
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
const metricConfig = getContext('metrics')
let ctx, canvasElement
const labels = metrics.filter(name => {
if (!jobMetrics.find(m => m.name == name && m.metric.scope == "node")) {
console.warn(`PolarPlot: No metric data for '${name}'`)
return false
}
return true
})
const getValuesForStat = (getStat) => labels.map(name => {
const peak = metricConfig(cluster, name).peak
const metric = jobMetrics.find(m => m.name == name && m.metric.scope == "node")
const value = getStat(metric.metric) / peak
return value <= 1. ? value : 1.
})
function getMax(metric) {
let max = 0
for (let series of metric.series)
max = Math.max(max, series.statistics.max)
return max
}
function getAvg(metric) {
let avg = 0
for (let series of metric.series)
avg += series.statistics.avg
return avg / metric.series.length
}
const data = [
{
name: 'Max',
values: getValuesForStat(getMax),
color: 'rgb(0, 102, 255)',
areaColor: 'rgba(0, 102, 255, 0.25)'
},
{
name: 'Avg',
values: getValuesForStat(getAvg),
color: 'rgb(255, 153, 0)',
areaColor: 'rgba(255, 153, 0, 0.25)'
}
]
function render() {
if (!width || Number.isNaN(width))
return
const centerX = width / 2
const centerY = height / 2 - 15
const radius = (Math.min(width, height) / 2) - 50
// Draw circles
ctx.lineWidth = 1
ctx.strokeStyle = '#999999'
ctx.beginPath()
ctx.arc(centerX, centerY, radius * 1.0, 0, Math.PI * 2, false)
ctx.stroke()
ctx.beginPath()
ctx.arc(centerX, centerY, radius * 0.666, 0, Math.PI * 2, false)
ctx.stroke()
ctx.beginPath()
ctx.arc(centerX, centerY, radius * 0.333, 0, Math.PI * 2, false)
ctx.stroke()
// Axis
ctx.font = `${fontSize}px ${fontFamily}`
ctx.textAlign = 'center'
ctx.fillText('1/3',
Math.floor(centerX + radius * 0.333),
Math.floor(centerY + 15))
ctx.fillText('2/3',
Math.floor(centerX + radius * 0.666),
Math.floor(centerY + 15))
ctx.fillText('1.0',
Math.floor(centerX + radius * 1.0),
Math.floor(centerY + 15))
// Label text and straight lines from center
for (let i = 0; i < labels.length; i++) {
const angle = 2 * Math.PI * ((i + 1) / labels.length)
const dx = Math.cos(angle) * radius
const dy = Math.sin(angle) * radius
ctx.fillText(labels[i],
Math.floor(centerX + dx * 1.1),
Math.floor(centerY + dy * 1.1))
ctx.beginPath()
ctx.moveTo(centerX, centerY)
ctx.lineTo(centerX + dx, centerY + dy)
ctx.stroke()
}
for (let dataset of data) {
console.assert(dataset.values.length === labels.length, 'this will look confusing')
ctx.fillStyle = dataset.color
ctx.strokeStyle = dataset.color
const points = []
for (let i = 0; i < dataset.values.length; i++) {
const value = dataset.values[i]
const angle = 2 * Math.PI * ((i + 1) / labels.length)
const x = centerX + Math.cos(angle) * radius * value
const y = centerY + Math.sin(angle) * radius * value
ctx.beginPath()
ctx.arc(x, y, 3, 0, Math.PI * 2, false)
ctx.fill()
points.push({ x, y })
}
// "Fill" the shape this dataset has
ctx.fillStyle = dataset.areaColor
ctx.beginPath()
ctx.moveTo(points[0].x, points[0].y)
for (let p of points)
ctx.lineTo(p.x, p.y)
ctx.lineTo(points[0].x, points[0].y)
ctx.stroke()
ctx.fill()
}
// Legend at the bottom left corner
ctx.textAlign = 'left'
let paddingLeft = 0
for (let dataset of data) {
const text = `${dataset.name}: `
const textWidth = ctx.measureText(text).width
ctx.fillStyle = 'black'
ctx.fillText(text, paddingLeft, height - 20)
ctx.fillStyle = dataset.color
ctx.beginPath()
ctx.arc(paddingLeft + textWidth + 5, height - 25, 5, 0, Math.PI * 2, false)
ctx.fill()
paddingLeft += textWidth + 15
}
ctx.fillStyle = 'black'
ctx.fillText(`Values relative to respective peak.`, 0, height - 7)
}
let mounted = false
onMount(() => {
canvasElement.width = width
canvasElement.height = height
ctx = canvasElement.getContext('2d')
render(ctx, data, width, height)
mounted = true
})
let timeoutId = null
function sizeChanged() {
if (!mounted)
return;
if (timeoutId != null)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
timeoutId = null
canvasElement.width = width
canvasElement.height = height
ctx = canvasElement.getContext('2d')
render(ctx, data, width, height)
}, 250)
}
$: sizeChanged(width, height)
</script>

View File

@@ -0,0 +1,355 @@
<div class="cc-plot">
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
</div>
<script context="module">
const axesColor = '#aaaaaa'
const fontSize = 12
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
const paddingLeft = 40,
paddingRight = 10,
paddingTop = 10,
paddingBottom = 50
function getGradientR(x) {
if (x < 0.5) return 0
if (x > 0.75) return 255
x = (x - 0.5) * 4.0
return Math.floor(x * 255.0)
}
function getGradientG(x) {
if (x > 0.25 && x < 0.75) return 255
if (x < 0.25) x = x * 4.0
else x = 1.0 - (x - 0.75) * 4.0
return Math.floor(x * 255.0)
}
function getGradientB(x) {
if (x < 0.25) return 255
if (x > 0.5) return 0
x = 1.0 - (x - 0.25) * 4.0
return Math.floor(x * 255.0)
}
function getRGB(c) {
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`
}
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
return {
x: x1 + a * (x2 - x1),
y: y1 + a * (y2 - y1)
}
}
const power = [1, 1e3, 1e6, 1e9, 1e12]
const suffix = ['', 'k', 'm', 'g']
function formatNumber(x) {
for (let i = 0; i < suffix.length; i++)
if (power[i] <= x && x < power[i+1])
return `${x / power[i]}${suffix[i]}`
return Math.abs(x) >= 1000 ? x.toExponential() : x.toString()
}
function axisStepFactor(i, size) {
if (size && size < 500)
return 10
if (i % 3 == 0)
return 2
else if (i % 3 == 1)
return 2.5
else
return 2
}
function render(ctx, data, cluster, width, height, colorDots, defaultMaxY) {
if (width <= 0)
return
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd || defaultMaxY]
const w = width - paddingLeft - paddingRight
const h = height - paddingTop - paddingBottom
// Helpers:
const [log10minX, log10maxX, log10minY, log10maxY] =
[Math.log10(minX), Math.log10(maxX), Math.log10(minY), Math.log10(maxY)]
/* Value -> Pixel-Coordinate */
const getCanvasX = (x) => {
x = Math.log10(x)
x -= log10minX; x /= (log10maxX - log10minX)
return Math.round((x * w) + paddingLeft)
}
const getCanvasY = (y) => {
y = Math.log10(y)
y -= log10minY
y /= (log10maxY - log10minY)
return Math.round((h - y * h) + paddingTop)
}
// Axes
ctx.fillStyle = 'black'
ctx.strokeStyle = axesColor
ctx.font = `${fontSize}px ${fontFamily}`
ctx.beginPath()
for (let x = minX, i = 0; x <= maxX; i++) {
let px = getCanvasX(x)
let text = formatNumber(x)
let textWidth = ctx.measureText(text).width
ctx.fillText(text,
Math.floor(px - (textWidth / 2)),
height - paddingBottom + fontSize + 5)
ctx.moveTo(px, paddingTop - 5)
ctx.lineTo(px, height - paddingBottom + 5)
x *= axisStepFactor(i, w)
}
if (data.xLabel) {
let textWidth = ctx.measureText(data.xLabel).width
ctx.fillText(data.xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20)
}
ctx.textAlign = 'center'
for (let y = minY, i = 0; y <= maxY; i++) {
let py = getCanvasY(y)
ctx.moveTo(paddingLeft - 5, py)
ctx.lineTo(width - paddingRight + 5, py)
ctx.save()
ctx.translate(paddingLeft - 10, py)
ctx.rotate(-Math.PI / 2)
ctx.fillText(formatNumber(y), 0, 0)
ctx.restore()
y *= axisStepFactor(i)
}
if (data.yLabel) {
ctx.save()
ctx.translate(15, Math.floor(height / 2))
ctx.rotate(-Math.PI / 2)
ctx.fillText(data.yLabel, 0, 0)
ctx.restore()
}
ctx.stroke()
// Draw Data
if (data.x && data.y) {
for (let i = 0; i < data.x.length; i++) {
let x = data.x[i], y = data.y[i], c = data.c[i]
if (x == null || y == null || Number.isNaN(x) || Number.isNaN(y))
continue
const s = 3
const px = getCanvasX(x)
const py = getCanvasY(y)
ctx.fillStyle = getRGB(c)
ctx.beginPath()
ctx.arc(px, py, s, 0, Math.PI * 2, false)
ctx.fill()
}
} else if (data.tiles) {
const rows = data.tiles.length
const cols = data.tiles[0].length
const tileWidth = Math.ceil(w / cols)
const tileHeight = Math.ceil(h / rows)
let max = data.tiles.reduce((max, row) =>
Math.max(max, row.reduce((max, val) =>
Math.max(max, val)), 0), 0)
if (max == 0)
max = 1
const tileColor = val => `rgba(255, 0, 0, ${(val / max)})`
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
let px = paddingLeft + (j / cols) * w
let py = paddingTop + (h - (i / rows) * h) - tileHeight
ctx.fillStyle = tileColor(data.tiles[i][j])
ctx.fillRect(px, py, tileWidth, tileHeight)
}
}
}
// Draw roofs
ctx.strokeStyle = 'black'
ctx.lineWidth = 2
ctx.beginPath()
if (cluster != null) {
const ycut = 0.01 * cluster.memoryBandwidth
const scalarKnee = (cluster.flopRateScalar - ycut) / cluster.memoryBandwidth
const simdKnee = (cluster.flopRateSimd - ycut) / cluster.memoryBandwidth
const scalarKneeX = getCanvasX(scalarKnee),
simdKneeX = getCanvasX(simdKnee),
flopRateScalarY = getCanvasY(cluster.flopRateScalar),
flopRateSimdY = getCanvasY(cluster.flopRateSimd)
if (scalarKneeX < width - paddingRight) {
ctx.moveTo(scalarKneeX, flopRateScalarY)
ctx.lineTo(width - paddingRight, flopRateScalarY)
}
if (simdKneeX < width - paddingRight) {
ctx.moveTo(simdKneeX, flopRateSimdY)
ctx.lineTo(width - paddingRight, flopRateSimdY)
}
let x1 = getCanvasX(0.01),
y1 = getCanvasY(ycut),
x2 = getCanvasX(simdKnee),
y2 = flopRateSimdY
let xAxisIntersect = lineIntersect(
x1, y1, x2, y2,
0, height - paddingBottom, width, height - paddingBottom)
if (xAxisIntersect.x > x1) {
x1 = xAxisIntersect.x
y1 = xAxisIntersect.y
}
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
}
ctx.stroke()
if (colorDots && data.x && data.y) {
// The Color Scale
ctx.fillStyle = 'black'
ctx.fillText('Time:', 17, height - 5)
const start = paddingLeft + 5
for (let x = start; x < width - paddingRight; x += 15) {
let c = (x - start) / (width - start - paddingRight)
ctx.fillStyle = getRGB(c)
ctx.beginPath()
ctx.arc(x, height - 10, 5, 0, Math.PI * 2, false)
ctx.fill()
}
}
}
function transformData(flopsAny, memBw, colorDots) {
const nodes = flopsAny.series.length
const timesteps = flopsAny.series[0].data.length
/* c will contain values from 0 to 1 representing the time */
const x = [], y = [], c = []
for (let i = 0; i < nodes; i++) {
const flopsData = flopsAny.series[i].data
const memBwData = memBw.series[i].data
for (let j = 0; j < timesteps; j++) {
const f = flopsData[j], m = memBwData[j]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(colorDots ? j / timesteps : 0)
}
}
return {
x, y, c,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
}
}
// Return something to be plotted. The argument shall be the result of the
// `nodeMetrics` GraphQL query.
export function transformPerNodeData(nodes) {
const x = [], y = [], c = []
for (let node of nodes) {
let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.metric.scope == 'node')?.metric
let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.metric.scope == 'node')?.metric
if (!flopsAny || !memBw)
continue
let flopsData = flopsAny.series[0].data, memBwData = memBw.series[0].data
const f = flopsData[flopsData.length - 1], m = memBwData[flopsData.length - 1]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(0)
}
return {
x, y, c,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
}
}
</script>
<script>
import { onMount, tick } from 'svelte'
export let flopsAny = null
export let memBw = null
export let cluster = null
export let maxY = null
export let width
export let height
export let tiles = null
export let colorDots = true
export let data = null
console.assert(data || tiles || (flopsAny && memBw), "you must provide flopsAny and memBw or tiles!")
let ctx, canvasElement, prevWidth = width, prevHeight = height
data = data != null ? data : (flopsAny && memBw
? transformData(flopsAny, memBw, colorDots)
: {
tiles: tiles,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
})
onMount(() => {
ctx = canvasElement.getContext('2d')
if (prevWidth != width || prevHeight != height) {
sizeChanged()
return
}
canvasElement.width = width
canvasElement.height = height
render(ctx, data, cluster, width, height, colorDots, maxY)
})
let timeoutId = null
function sizeChanged() {
if (!ctx)
return
if (timeoutId != null)
clearTimeout(timeoutId)
prevWidth = width
prevHeight = height
timeoutId = setTimeout(() => {
if (!canvasElement)
return
timeoutId = null
canvasElement.width = width
canvasElement.height = height
render(ctx, data, cluster, width, height, colorDots, maxY)
}, 250)
}
$: sizeChanged(width, height)
</script>

View File

@@ -0,0 +1,171 @@
<div class="cc-plot">
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
</div>
<script context="module">
import { formatNumber } from '../utils.js'
const axesColor = '#aaaaaa'
const fontSize = 12
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
const paddingLeft = 40,
paddingRight = 10,
paddingTop = 10,
paddingBottom = 50
function getStepSize(valueRange, pixelRange, minSpace) {
const proposition = valueRange / (pixelRange / minSpace);
const getStepSize = n => Math.pow(10, Math.floor(n / 3)) *
(n < 0 ? [1., 5., 2.][-n % 3] : [1., 2., 5.][n % 3]);
let n = 0;
let stepsize = getStepSize(n);
while (true) {
let bigger = getStepSize(n + 1);
if (proposition > bigger) {
n += 1;
stepsize = bigger;
} else {
return stepsize;
}
}
}
function render(ctx, X, Y, S, color, xLabel, yLabel, width, height) {
if (width <= 0)
return;
const [minX, minY] = [0., 0.];
let maxX = X.reduce((maxX, x) => Math.max(maxX, x), minX);
let maxY = Y.reduce((maxY, y) => Math.max(maxY, y), minY);
const w = width - paddingLeft - paddingRight;
const h = height - paddingTop - paddingBottom;
if (maxX == 0 && maxY == 0) {
maxX = 1;
maxY = 1;
}
/* Value -> Pixel-Coordinate */
const getCanvasX = (x) => {
x -= minX; x /= (maxX - minX);
return Math.round((x * w) + paddingLeft);
};
const getCanvasY = (y) => {
y -= minY; y /= (maxY - minY);
return Math.round((h - y * h) + paddingTop);
};
// Draw Data
let size = 3
if (S) {
let max = S.reduce((max, s, i) => (X[i] == null || Y[i] == null || Number.isNaN(X[i]) || Number.isNaN(Y[i])) ? max : Math.max(max, s), 0)
size = (w / 15) / max
}
ctx.fillStyle = color;
for (let i = 0; i < X.length; i++) {
let x = X[i], y = Y[i];
if (x == null || y == null || Number.isNaN(x) || Number.isNaN(y))
continue;
const s = S ? S[i] * size : size;
const px = getCanvasX(x);
const py = getCanvasY(y);
ctx.beginPath();
ctx.arc(px, py, s, 0, Math.PI * 2, false);
ctx.fill();
}
// Axes
ctx.fillStyle = '#000000'
ctx.strokeStyle = axesColor;
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.beginPath();
const stepsizeX = getStepSize(maxX, w, 75);
for (let x = minX, i = 0; x <= maxX; i++) {
let px = getCanvasX(x);
let text = formatNumber(x);
let textWidth = ctx.measureText(text).width;
ctx.fillText(text,
Math.floor(px - (textWidth / 2)),
height - paddingBottom + fontSize + 5);
ctx.moveTo(px, paddingTop - 5);
ctx.lineTo(px, height - paddingBottom + 5);
x += stepsizeX;
}
if (xLabel) {
let textWidth = ctx.measureText(xLabel).width;
ctx.fillText(xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20);
}
ctx.textAlign = 'center';
const stepsizeY = getStepSize(maxY, h, 75);
for (let y = minY, i = 0; y <= maxY; i++) {
let py = getCanvasY(y);
ctx.moveTo(paddingLeft - 5, py);
ctx.lineTo(width - paddingRight + 5, py);
ctx.save();
ctx.translate(paddingLeft - 10, py);
ctx.rotate(-Math.PI / 2);
ctx.fillText(formatNumber(y), 0, 0);
ctx.restore();
y += stepsizeY;
}
if (yLabel) {
ctx.save();
ctx.translate(15, Math.floor(height / 2));
ctx.rotate(-Math.PI / 2);
ctx.fillText(yLabel, 0, 0);
ctx.restore();
}
ctx.stroke();
}
</script>
<script>
import { onMount } from 'svelte';
export let X;
export let Y;
export let S = null;
export let color = '#0066cc';
export let width;
export let height;
export let xLabel;
export let yLabel;
let ctx;
let canvasElement;
onMount(() => {
canvasElement.width = width;
canvasElement.height = height;
ctx = canvasElement.getContext('2d');
render(ctx, X, Y, S, color, xLabel, yLabel, width, height);
});
let timeoutId = null;
function sizeChanged() {
if (timeoutId != null)
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (!canvasElement)
return;
canvasElement.width = width;
canvasElement.height = height;
ctx = canvasElement.getContext('2d');
render(ctx, X, Y, S, color, xLabel, yLabel, width, height);
}, 250);
}
$: sizeChanged(width, height);
</script>