mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-22 20:41:40 +02:00
adds new roofline component for job average based data
- clickable, resource sized and duration colored bubbles
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
import UsageDash from "./status/UsageDash.svelte";
|
||||
import NodeDash from "./status/NodeDash.svelte";
|
||||
import StatisticsDash from "./status/StatisticsDash.svelte";
|
||||
import DevelDash from "./status/DevelDash.svelte";
|
||||
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
@@ -68,7 +69,13 @@
|
||||
|
||||
<Card class="overflow-auto" style="height: auto;">
|
||||
<TabContent>
|
||||
<TabPane tabId="status-dash" tab="Status" active>
|
||||
<TabPane tabId="devel-dash" tab="Devel" active>
|
||||
<CardBody>
|
||||
<DevelDash {cluster}></DevelDash>
|
||||
</CardBody>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tabId="status-dash" tab="Status">
|
||||
<CardBody>
|
||||
<StatusDash {cluster}></StatusDash>
|
||||
</CardBody>
|
||||
|
739
web/frontend/src/generic/plots/NewBubbleRoofline.svelte
Normal file
739
web/frontend/src/generic/plots/NewBubbleRoofline.svelte
Normal file
@@ -0,0 +1,739 @@
|
||||
<!--
|
||||
@component Roofline Model Plot based on uPlot
|
||||
|
||||
Properties:
|
||||
- `data [null, [], []]`: Roofline Data Structure, see below for details [Default: null]
|
||||
- `allowSizeChange Bool?`: If dimensions of rendered plot can change [Default: false]
|
||||
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
|
||||
- `width Number?`: Plot width (reactively adaptive) [Default: 600]
|
||||
- `height Number?`: Plot height (reactively adaptive) [Default: 380]
|
||||
|
||||
Data Format:
|
||||
- `data = [null, [], []]`
|
||||
- Index 0: null-axis required for scatter
|
||||
- Index 1: Array of XY-Arrays for Scatter
|
||||
- Index 2: Optional Time Info
|
||||
- `data[1][0] = [100, 200, 500, ...]`
|
||||
- X Axis: Intensity (Vals up to clusters' flopRateScalar value)
|
||||
- `data[1][1] = [1000, 2000, 1500, ...]`
|
||||
- Y Axis: Performance (Vals up to clusters' flopRateSimd value)
|
||||
- `data[2] = [0.1, 0.15, 0.2, ...]`
|
||||
- Color Code: Time Information (Floats from 0 to 1) (Optional)
|
||||
-->
|
||||
<script>
|
||||
import uPlot from "uplot";
|
||||
import { formatNumber } from "../units.js";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { Card } from "@sveltestrap/sveltestrap";
|
||||
import { roundTwoDigits } from "../units.js";
|
||||
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
roofData = null,
|
||||
jobsData = null,
|
||||
allowSizeChange = false,
|
||||
subCluster = null,
|
||||
width = 600,
|
||||
height = 380,
|
||||
} = $props();
|
||||
|
||||
$inspect(jobsData)
|
||||
|
||||
/* Const Init */
|
||||
const lineWidth = clusterCockpitConfig?.plot_general_lineWidth || 2;
|
||||
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
|
||||
|
||||
/* Var Init */
|
||||
let timeoutId = null;
|
||||
|
||||
/* State Init */
|
||||
let plotWrapper = $state(null);
|
||||
let uplot = $state(null);
|
||||
|
||||
/* Effect */
|
||||
$effect(() => {
|
||||
if (allowSizeChange) sizeChanged(width, height);
|
||||
});
|
||||
|
||||
// Copied Example Vars for Uplot Bubble
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
|
||||
let qt;
|
||||
let pxRatio;
|
||||
function setPxRatio() {
|
||||
pxRatio = uPlot.pxRatio;
|
||||
}
|
||||
setPxRatio();
|
||||
window.addEventListener('dppxchange', setPxRatio);
|
||||
// let minSize = 6;
|
||||
let maxSize = 60;
|
||||
let maxArea = Math.PI * (maxSize / 2) ** 2;
|
||||
// let minArea = Math.PI * (minSize / 2) ** 2;
|
||||
|
||||
/* Functions */
|
||||
// Helper
|
||||
function pointWithin(px, py, rlft, rtop, rrgt, rbtm) {
|
||||
return px >= rlft && px <= rrgt && py >= rtop && py <= rbtm;
|
||||
}
|
||||
|
||||
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, makeTransparent = false) {
|
||||
if (makeTransparent) return `rgb(${cbmode ? '0' : getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)}, 0.33)`;
|
||||
else return `rgb(${cbmode ? '0' : getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`;
|
||||
}
|
||||
function nearestThousand(num) {
|
||||
return Math.ceil(num / 1000) * 1000;
|
||||
}
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
// quadratic scaling (px area)
|
||||
function getSize(value, minValue, maxValue) {
|
||||
let pct = value / maxValue;
|
||||
// clamp to min area
|
||||
//let area = Math.max(maxArea * pct, minArea);
|
||||
let area = maxArea * pct;
|
||||
return Math.sqrt(area / Math.PI) * 2;
|
||||
}
|
||||
|
||||
function getSizeMinMax(u) {
|
||||
let minValue = Infinity;
|
||||
let maxValue = -Infinity;
|
||||
for (let i = 1; i < u.series.length; i++) {
|
||||
let sizeData = u.data[i][2];
|
||||
for (let j = 0; j < sizeData.length; j++) {
|
||||
minValue = Math.min(minValue, sizeData[j]);
|
||||
maxValue = Math.max(maxValue, sizeData[j]);
|
||||
}
|
||||
}
|
||||
return [minValue, maxValue];
|
||||
}
|
||||
|
||||
// Quadtree Object (How to import?)
|
||||
class Quadtree {
|
||||
constructor (x, y, w, h, l) {
|
||||
let t = this;
|
||||
t.x = x;
|
||||
t.y = y;
|
||||
t.w = w;
|
||||
t.h = h;
|
||||
t.l = l || 0;
|
||||
t.o = [];
|
||||
t.q = null;
|
||||
t.MAX_OBJECTS = 10;
|
||||
t.MAX_LEVELS = 4;
|
||||
};
|
||||
|
||||
get quadtree() {
|
||||
return "Implement me!";
|
||||
}
|
||||
|
||||
split() {
|
||||
let t = this,
|
||||
x = t.x,
|
||||
y = t.y,
|
||||
w = t.w / 2,
|
||||
h = t.h / 2,
|
||||
l = t.l + 1;
|
||||
|
||||
t.q = [
|
||||
// top right
|
||||
new Quadtree(x + w, y, w, h, l),
|
||||
// top left
|
||||
new Quadtree(x, y, w, h, l),
|
||||
// bottom left
|
||||
new Quadtree(x, y + h, w, h, l),
|
||||
// bottom right
|
||||
new Quadtree(x + w, y + h, w, h, l),
|
||||
];
|
||||
};
|
||||
|
||||
quads(x, y, w, h, cb) {
|
||||
let t = this,
|
||||
q = t.q,
|
||||
hzMid = t.x + t.w / 2,
|
||||
vtMid = t.y + t.h / 2,
|
||||
startIsNorth = y < vtMid,
|
||||
startIsWest = x < hzMid,
|
||||
endIsEast = x + w > hzMid,
|
||||
endIsSouth = y + h > vtMid;
|
||||
|
||||
// top-right quad
|
||||
startIsNorth && endIsEast && cb(q[0]);
|
||||
// top-left quad
|
||||
startIsWest && startIsNorth && cb(q[1]);
|
||||
// bottom-left quad
|
||||
startIsWest && endIsSouth && cb(q[2]);
|
||||
// bottom-right quad
|
||||
endIsEast && endIsSouth && cb(q[3]);
|
||||
};
|
||||
|
||||
add(o) {
|
||||
let t = this;
|
||||
|
||||
if (t.q != null) {
|
||||
t.quads(o.x, o.y, o.w, o.h, q => {
|
||||
q.add(o);
|
||||
});
|
||||
}
|
||||
else {
|
||||
let os = t.o;
|
||||
|
||||
os.push(o);
|
||||
|
||||
if (os.length > t.MAX_OBJECTS && t.l < t.MAX_LEVELS) {
|
||||
t.split();
|
||||
|
||||
for (let i = 0; i < os.length; i++) {
|
||||
let oi = os[i];
|
||||
|
||||
t.quads(oi.x, oi.y, oi.w, oi.h, q => {
|
||||
q.add(oi);
|
||||
});
|
||||
}
|
||||
|
||||
t.o.length = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
get(x, y, w, h, cb) {
|
||||
let t = this;
|
||||
let os = t.o;
|
||||
|
||||
for (let i = 0; i < os.length; i++)
|
||||
cb(os[i]);
|
||||
|
||||
if (t.q != null) {
|
||||
t.quads(x, y, w, h, q => {
|
||||
q.get(x, y, w, h, cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.o.length = 0;
|
||||
this.q = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Dot Renderers
|
||||
const makeDrawPoints = (opts) => {
|
||||
let {/*size,*/ disp, each = () => {}} = opts;
|
||||
const sizeBase = 5 * pxRatio;
|
||||
|
||||
return (u, seriesIdx, idx0, idx1) => {
|
||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||
let d = u.data[seriesIdx];
|
||||
|
||||
let strokeWidth = 2;
|
||||
|
||||
u.ctx.save();
|
||||
|
||||
u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
||||
u.ctx.clip();
|
||||
|
||||
// u.ctx.fillStyle = series.fill();
|
||||
// u.ctx.strokeStyle = series.stroke();
|
||||
u.ctx.lineWidth = strokeWidth;
|
||||
|
||||
let deg360 = 2 * Math.PI;
|
||||
|
||||
// console.time("points");
|
||||
|
||||
// let cir = new Path2D();
|
||||
// cir.moveTo(0, 0);
|
||||
// arc(cir, 0, 0, 3, 0, deg360);
|
||||
|
||||
// Create transformation matrix that moves 200 points to the right
|
||||
// let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
|
||||
// m.a = 1; m.b = 0;
|
||||
// m.c = 0; m.d = 1;
|
||||
// m.e = 200; m.f = 0;
|
||||
|
||||
// compute bubble dims
|
||||
// let sizes = disp.size.values(u, seriesIdx, idx0, idx1);
|
||||
|
||||
// todo: this depends on direction & orientation
|
||||
// todo: calc once per redraw, not per path
|
||||
let filtLft = u.posToVal(-maxSize / 2, scaleX.key);
|
||||
let filtRgt = u.posToVal(u.bbox.width / pxRatio + maxSize / 2, scaleX.key);
|
||||
let filtBtm = u.posToVal(u.bbox.height / pxRatio + maxSize / 2, scaleY.key);
|
||||
let filtTop = u.posToVal(-maxSize / 2, scaleY.key);
|
||||
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
// Import from Roofline
|
||||
u.ctx.strokeStyle = getRGB(u.data[2][i]);
|
||||
u.ctx.fillStyle = getRGB(u.data[2][i], true);
|
||||
// End
|
||||
|
||||
let xVal = d[0][i];
|
||||
let yVal = d[1][i];
|
||||
const size = sizeBase + (jobsData[i]?.numAcc ? jobsData[i].numAcc / 2 : jobsData[i].numNodes);
|
||||
// let size = sizes[i] * pxRatio;
|
||||
|
||||
if (xVal >= filtLft && xVal <= filtRgt && yVal >= filtBtm && yVal <= filtTop) {
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
|
||||
u.ctx.moveTo(cx + size/2, cy);
|
||||
u.ctx.beginPath();
|
||||
u.ctx.arc(cx, cy, size/2, 0, deg360);
|
||||
u.ctx.fill();
|
||||
u.ctx.stroke();
|
||||
|
||||
each(u, seriesIdx, i,
|
||||
cx - size/2 - strokeWidth/2,
|
||||
cy - size/2 - strokeWidth/2,
|
||||
size + strokeWidth,
|
||||
size + strokeWidth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// console.timeEnd("points");
|
||||
|
||||
u.ctx.restore();
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
};
|
||||
|
||||
let drawPoints = makeDrawPoints({
|
||||
disp: {
|
||||
size: {
|
||||
unit: 3, // raw CSS pixels
|
||||
// discr: true,
|
||||
values: (u, seriesIdx, idx0, idx1) => {
|
||||
// TODO: only run once per setData() call
|
||||
let [minValue, maxValue] = getSizeMinMax(u);
|
||||
return u.data[seriesIdx][2].map(v => getSize(v, minValue, maxValue));
|
||||
},
|
||||
},
|
||||
},
|
||||
each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => {
|
||||
// we get back raw canvas coords (included axes & padding). translate to the plotting area origin
|
||||
lft -= u.bbox.left;
|
||||
top -= u.bbox.top;
|
||||
qt.add({x: lft, y: top, w: wid, h: hgt, sidx: seriesIdx, didx: dataIdx});
|
||||
},
|
||||
});
|
||||
|
||||
const legendValues = (u, seriesIdx, dataIdx) => {
|
||||
// when data null, it's initial schema probe (also u.status == 0)
|
||||
if (u.data == null || dataIdx == null || hRect == null || hRect.sidx != seriesIdx) {
|
||||
return {
|
||||
"Intensity [FLOPS/Byte]": '-',
|
||||
"":'',
|
||||
"Performace [GFLOPS]": '-'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
"Intensity [FLOPS/Byte]": roundTwoDigits(u.data[seriesIdx][0][dataIdx]),
|
||||
"":'',
|
||||
"Performace [GFLOPS]": roundTwoDigits(u.data[seriesIdx][1][dataIdx]),
|
||||
};
|
||||
};
|
||||
|
||||
// Tooltip Plugin
|
||||
function tooltipPlugin({onclick, getJobData, shiftX = 10, shiftY = 10}) {
|
||||
let tooltipLeftOffset = 0;
|
||||
let tooltipTopOffset = 0;
|
||||
|
||||
const tooltip = document.createElement("div");
|
||||
|
||||
// Build Manual Class By Styles
|
||||
tooltip.style.fontSize = "10pt";
|
||||
tooltip.style.position = "absolute";
|
||||
tooltip.style.background = "#fcfcfc";
|
||||
tooltip.style.display = "nonde";
|
||||
tooltip.style.border = "2px solid black";
|
||||
tooltip.style.padding = "4px";
|
||||
tooltip.style.pointerEvents = "none";
|
||||
tooltip.style.zIndex = "100";
|
||||
tooltip.style.whiteSpace = "pre";
|
||||
tooltip.style.fontFamily = "monospace";
|
||||
|
||||
const tipSeriesIdx = 1; // Scatter: Series IDX is always 1
|
||||
let tipDataIdx = null;
|
||||
|
||||
// const fmtDate = uPlot.fmtDate("{M}/{D}/{YY} {h}:{mm}:{ss} {AA}");
|
||||
let over;
|
||||
let tooltipVisible = false;
|
||||
|
||||
function showTooltip() {
|
||||
if (!tooltipVisible) {
|
||||
tooltip.style.display = "block";
|
||||
over.style.cursor = "pointer";
|
||||
tooltipVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
if (tooltipVisible) {
|
||||
tooltip.style.display = "none";
|
||||
over.style.cursor = null;
|
||||
tooltipVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setTooltip(u, i) {
|
||||
showTooltip();
|
||||
|
||||
let top = u.valToPos(u.data[tipSeriesIdx][1][i], 'y');
|
||||
let lft = u.valToPos(u.data[tipSeriesIdx][0][i], 'x');
|
||||
|
||||
tooltip.style.top = (tooltipTopOffset + top + shiftX) + "px";
|
||||
tooltip.style.left = (tooltipLeftOffset + lft + shiftY) + "px";
|
||||
|
||||
tooltip.style.borderColor = getRGB(u.data[2][i]);
|
||||
tooltip.textContent = (
|
||||
// Tooltip Content as String
|
||||
`Job ID: ${getJobData(u, i).jobId}\nNodes: ${getJobData(u, i).numNodes}${getJobData(u, i)?.numAcc?`\nAccelerators: ${getJobData(u, i).numAcc}`:''}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
ready: [
|
||||
u => {
|
||||
over = u.over;
|
||||
tooltipLeftOffset = parseFloat(over.style.left);
|
||||
tooltipTopOffset = parseFloat(over.style.top);
|
||||
u.root.querySelector(".u-wrap").appendChild(tooltip);
|
||||
|
||||
let clientX;
|
||||
let clientY;
|
||||
|
||||
over.addEventListener("mousedown", e => {
|
||||
clientX = e.clientX;
|
||||
clientY = e.clientY;
|
||||
});
|
||||
|
||||
over.addEventListener("mouseup", e => {
|
||||
// clicked in-place
|
||||
if (e.clientX == clientX && e.clientY == clientY) {
|
||||
if (tipDataIdx != null) {
|
||||
onclick(u, tipDataIdx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
],
|
||||
setCursor: [
|
||||
u => {
|
||||
let i = u.legend.idxs[1];
|
||||
if (i != null) {
|
||||
tipDataIdx = i;
|
||||
setTooltip(u, i);
|
||||
} else {
|
||||
tipDataIdx = null;
|
||||
hideTooltip();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Main Functions
|
||||
function sizeChanged() {
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null;
|
||||
if (uplot) uplot.destroy();
|
||||
render(roofData, jobsData);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
let hRect;
|
||||
function render(roofdata, jobsData) {
|
||||
if (roofdata) {
|
||||
const opts = {
|
||||
title: "Job Average Roofline Diagram (Bubble)",
|
||||
mode: 2,
|
||||
width: width,
|
||||
height: height,
|
||||
legend: {
|
||||
// show: true,
|
||||
},
|
||||
cursor: {
|
||||
drag: { x: true, y: false }, // Activate zoom
|
||||
dataIdx: (u, seriesIdx) => {
|
||||
if (seriesIdx == 1) {
|
||||
hRect = null;
|
||||
|
||||
let dist = Infinity;
|
||||
let area = Infinity;
|
||||
let cx = u.cursor.left * pxRatio;
|
||||
let cy = u.cursor.top * pxRatio;
|
||||
|
||||
qt.get(cx, cy, 1, 1, o => {
|
||||
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) {
|
||||
let ocx = o.x + o.w / 2;
|
||||
let ocy = o.y + o.h / 2;
|
||||
|
||||
let dx = ocx - cx;
|
||||
let dy = ocy - cy;
|
||||
|
||||
let d = Math.sqrt(dx ** 2 + dy ** 2);
|
||||
|
||||
// test against radius for actual hover
|
||||
if (d <= o.w / 2) {
|
||||
let a = o.w * o.h;
|
||||
|
||||
// prefer smallest
|
||||
if (a < area) {
|
||||
area = a;
|
||||
dist = d;
|
||||
hRect = o;
|
||||
}
|
||||
// only hover bbox with closest distance
|
||||
else if (a == area && d <= dist) {
|
||||
dist = d;
|
||||
hRect = o;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return hRect && seriesIdx == hRect.sidx ? hRect.didx : null;
|
||||
},
|
||||
points: {
|
||||
size: (u, seriesIdx) => {
|
||||
return hRect && seriesIdx == hRect.sidx ? hRect.w / pxRatio : 0;
|
||||
}
|
||||
},
|
||||
focus: {
|
||||
prox: 1e3,
|
||||
alpha: 0.3,
|
||||
dist: (u, seriesIdx) => {
|
||||
let prox = (hRect?.sidx === seriesIdx ? 0 : Infinity);
|
||||
return prox;
|
||||
},
|
||||
}
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
label: "Intensity [FLOPS/Byte]",
|
||||
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||
},
|
||||
{
|
||||
label: "Performace [GFLOPS]",
|
||||
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||
},
|
||||
],
|
||||
scales: {
|
||||
x: {
|
||||
time: false,
|
||||
range: [0.01, 1000],
|
||||
distr: 3, // Render as log
|
||||
log: 10, // log exp
|
||||
},
|
||||
y: {
|
||||
range: [
|
||||
0.01,
|
||||
subCluster?.flopRateSimd?.value
|
||||
? nearestThousand(subCluster.flopRateSimd.value)
|
||||
: 10000,
|
||||
],
|
||||
distr: 3, // Render as log
|
||||
log: 10, // log exp
|
||||
},
|
||||
},
|
||||
series: [
|
||||
null,
|
||||
{
|
||||
facets: [
|
||||
{
|
||||
scale: 'x',
|
||||
auto: true,
|
||||
},
|
||||
{
|
||||
scale: 'y',
|
||||
auto: true,
|
||||
}
|
||||
],
|
||||
paths: drawPoints,
|
||||
values: legendValues
|
||||
}
|
||||
],
|
||||
hooks: {
|
||||
// setSeries: [ (u, seriesIdx) => console.log('setSeries', seriesIdx) ],
|
||||
// setLegend: [ u => console.log('setLegend', u.legend.idxs) ],
|
||||
drawClear: [
|
||||
(u) => {
|
||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||
qt.clear();
|
||||
|
||||
u.series.forEach((s, i) => {
|
||||
if (i > 0) s._paths = null;
|
||||
});
|
||||
},
|
||||
],
|
||||
draw: [
|
||||
(u) => {
|
||||
// draw roofs when subCluster set
|
||||
if (subCluster != null) {
|
||||
const padding = u._padding; // [top, right, bottom, left]
|
||||
|
||||
u.ctx.strokeStyle = "black";
|
||||
u.ctx.lineWidth = lineWidth;
|
||||
u.ctx.beginPath();
|
||||
|
||||
const ycut = 0.01 * subCluster.memoryBandwidth.value;
|
||||
const scalarKnee =
|
||||
(subCluster.flopRateScalar.value - ycut) /
|
||||
subCluster.memoryBandwidth.value;
|
||||
const simdKnee =
|
||||
(subCluster.flopRateSimd.value - ycut) /
|
||||
subCluster.memoryBandwidth.value;
|
||||
const scalarKneeX = u.valToPos(scalarKnee, "x", true), // Value, axis, toCanvasPixels
|
||||
simdKneeX = u.valToPos(simdKnee, "x", true),
|
||||
flopRateScalarY = u.valToPos(
|
||||
subCluster.flopRateScalar.value,
|
||||
"y",
|
||||
true,
|
||||
),
|
||||
flopRateSimdY = u.valToPos(
|
||||
subCluster.flopRateSimd.value,
|
||||
"y",
|
||||
true,
|
||||
);
|
||||
|
||||
if (
|
||||
scalarKneeX <
|
||||
width * window.devicePixelRatio -
|
||||
padding[1] * window.devicePixelRatio
|
||||
) {
|
||||
// Lower horizontal roofline
|
||||
u.ctx.moveTo(scalarKneeX, flopRateScalarY);
|
||||
u.ctx.lineTo(
|
||||
width * window.devicePixelRatio -
|
||||
padding[1] * window.devicePixelRatio,
|
||||
flopRateScalarY,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
simdKneeX <
|
||||
width * window.devicePixelRatio -
|
||||
padding[1] * window.devicePixelRatio
|
||||
) {
|
||||
// Top horitontal roofline
|
||||
u.ctx.moveTo(simdKneeX, flopRateSimdY);
|
||||
u.ctx.lineTo(
|
||||
width * window.devicePixelRatio -
|
||||
padding[1] * window.devicePixelRatio,
|
||||
flopRateSimdY,
|
||||
);
|
||||
}
|
||||
|
||||
let x1 = u.valToPos(0.01, "x", true),
|
||||
y1 = u.valToPos(ycut, "y", true);
|
||||
|
||||
let x2 = u.valToPos(simdKnee, "x", true),
|
||||
y2 = flopRateSimdY;
|
||||
|
||||
let xAxisIntersect = lineIntersect(
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
u.valToPos(0.01, "x", true),
|
||||
u.valToPos(1.0, "y", true), // X-Axis Start Coords
|
||||
u.valToPos(1000, "x", true),
|
||||
u.valToPos(1.0, "y", true), // X-Axis End Coords
|
||||
);
|
||||
|
||||
if (xAxisIntersect.x > x1) {
|
||||
x1 = xAxisIntersect.x;
|
||||
y1 = xAxisIntersect.y;
|
||||
}
|
||||
|
||||
// Diagonal
|
||||
u.ctx.moveTo(x1, y1);
|
||||
u.ctx.lineTo(x2, y2);
|
||||
|
||||
u.ctx.stroke();
|
||||
// Reset grid lineWidth
|
||||
u.ctx.lineWidth = 0.15;
|
||||
}
|
||||
|
||||
// The Color Scale For Time Information
|
||||
const posX = u.valToPos(0.1, "x", true)
|
||||
const posXLimit = u.valToPos(100, "x", true)
|
||||
const posY = u.valToPos(14000.0, "y", true)
|
||||
u.ctx.fillStyle = 'black'
|
||||
u.ctx.fillText('Short', posX, posY)
|
||||
const start = posX + 10
|
||||
for (let x = start; x < posXLimit; x += 10) {
|
||||
let c = (x - start) / (posXLimit - start)
|
||||
u.ctx.fillStyle = getRGB(c)
|
||||
u.ctx.beginPath()
|
||||
u.ctx.arc(x, posY, 3, 0, Math.PI * 2, false)
|
||||
u.ctx.fill()
|
||||
}
|
||||
u.ctx.fillStyle = 'black'
|
||||
u.ctx.fillText('Long', posXLimit + 23, posY)
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
tooltipPlugin({
|
||||
onclick(u, dataIdx) {
|
||||
window.open(`/monitoring/job/${jobsData[dataIdx].id}`);
|
||||
},
|
||||
getJobData: (u, dataIdx) => { return jobsData[dataIdx] }
|
||||
}),
|
||||
],
|
||||
};
|
||||
uplot = new uPlot(opts, roofdata, plotWrapper);
|
||||
} else {
|
||||
// console.log("No data for roofline!");
|
||||
}
|
||||
}
|
||||
|
||||
/* On Mount */
|
||||
onMount(() => {
|
||||
render(roofData, jobsData);
|
||||
});
|
||||
|
||||
/* On Destroy */
|
||||
onDestroy(() => {
|
||||
if (uplot) uplot.destroy();
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if roofData != null}
|
||||
<div bind:this={plotWrapper} class="p-2"></div>
|
||||
{:else}
|
||||
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
|
||||
>
|
||||
{/if}
|
164
web/frontend/src/status/DevelDash.svelte
Normal file
164
web/frontend/src/status/DevelDash.svelte
Normal file
@@ -0,0 +1,164 @@
|
||||
<!--
|
||||
@component Main cluster status view component; renders current system-usage information
|
||||
|
||||
Properties:
|
||||
- `cluster String`: The cluster to show status information for
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
getContextClient,
|
||||
} from "@urql/svelte";
|
||||
import {
|
||||
init,
|
||||
} from "../generic/utils.js";
|
||||
import Roofline from "../generic/plots/Roofline.svelte";
|
||||
import NewBubbleRoofline from "../generic/plots/NewBubbleRoofline.svelte";
|
||||
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
cluster
|
||||
} = $props();
|
||||
|
||||
/* Const Init */
|
||||
const { query: initq } = init();
|
||||
const client = getContextClient();
|
||||
|
||||
/* State Init */
|
||||
// let from = $state(new Date(Date.now() - 5 * 60 * 1000));
|
||||
// let to = $state(new Date(Date.now()));
|
||||
let plotWidths = $state([]);
|
||||
|
||||
/* Derived */
|
||||
// Note: nodeMetrics are requested on configured $timestep resolution
|
||||
// Result: The latest 5 minutes (datapoints) for each node independent of job
|
||||
const jobRoofQuery = $derived(queryStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
query ($filter: [JobFilter!]!, $metrics: [String!]!) {
|
||||
jobsMetricStats(filter: $filter, metrics: $metrics) {
|
||||
id
|
||||
jobId
|
||||
duration
|
||||
numNodes
|
||||
numAccelerators
|
||||
subCluster
|
||||
stats {
|
||||
name
|
||||
data {
|
||||
avg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
|
||||
metrics: ["flops_any", "mem_bw"], // Fixed names for job roofline
|
||||
},
|
||||
}));
|
||||
|
||||
/* Function */
|
||||
function transformJobsStatsToData(subclusterData) {
|
||||
/* c will contain values from 0 to 1 representing the duration */
|
||||
let data = null
|
||||
const x = [], y = [], c = [], day = 86400.0
|
||||
|
||||
if (subclusterData) {
|
||||
for (let i = 0; i < subclusterData.length; i++) {
|
||||
const flopsData = subclusterData[i].stats.find((s) => s.name == "flops_any")
|
||||
const memBwData = subclusterData[i].stats.find((s) => s.name == "mem_bw")
|
||||
|
||||
const f = flopsData.data.avg
|
||||
const m = memBwData.data.avg
|
||||
const d = subclusterData[i].duration / day
|
||||
|
||||
const intensity = f / m
|
||||
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
|
||||
continue
|
||||
|
||||
x.push(intensity)
|
||||
y.push(f)
|
||||
// Long Jobs > 1 Day: Use max Color
|
||||
if (d > 1.0) c.push(1.0)
|
||||
else c.push(d)
|
||||
}
|
||||
} else {
|
||||
console.warn("transformData: metrics for 'mem_bw' and/or 'flops_any' missing!")
|
||||
}
|
||||
|
||||
if (x.length > 0 && y.length > 0 && c.length > 0) {
|
||||
data = [null, [x, y], c] // for dataformat see roofline.svelte
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
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} })
|
||||
} else {
|
||||
console.warn("transformData: jobInfo missing!")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!-- Gauges & Roofline per Subcluster-->
|
||||
{#if $initq.data && $jobRoofQuery.data}
|
||||
{#each $initq.data.clusters.find((c) => c.name == cluster).subClusters as subCluster, i}
|
||||
<Row cols={{ lg: 2, md: 2 , sm: 1}} class="mb-3 justify-content-center">
|
||||
<Col class="px-3 mt-2 mt-lg-0">
|
||||
<b>Classic</b>
|
||||
<div bind:clientWidth={plotWidths[i]}>
|
||||
{#key $jobRoofQuery.data.jobsMetricStats}
|
||||
<b>{subCluster.name} Total: {$jobRoofQuery.data.jobsMetricStats.filter(
|
||||
(data) => data.subCluster == subCluster.name,
|
||||
).length} Jobs</b>
|
||||
<Roofline
|
||||
allowSizeChange
|
||||
renderTime
|
||||
width={plotWidths[i] - 10}
|
||||
height={300}
|
||||
subCluster={subCluster}
|
||||
data={transformJobsStatsToData($jobRoofQuery?.data?.jobsMetricStats.filter(
|
||||
(data) => data.subCluster == subCluster.name,
|
||||
)
|
||||
)}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</Col>
|
||||
<Col class="px-3 mt-2 mt-lg-0">
|
||||
<b>Bubble</b>
|
||||
<div bind:clientWidth={plotWidths[i]}>
|
||||
{#key $jobRoofQuery.data.jobsMetricStats}
|
||||
<b>{subCluster.name} Total: {$jobRoofQuery.data.jobsMetricStats.filter(
|
||||
(data) => data.subCluster == subCluster.name,
|
||||
).length} Jobs</b>
|
||||
<NewBubbleRoofline
|
||||
allowSizeChange
|
||||
width={plotWidths[i] - 10}
|
||||
height={300}
|
||||
subCluster={subCluster}
|
||||
roofData={transformJobsStatsToData($jobRoofQuery?.data?.jobsMetricStats.filter(
|
||||
(data) => data.subCluster == subCluster.name,
|
||||
)
|
||||
)}
|
||||
jobsData={transformJobsStatsToInfo($jobRoofQuery?.data?.jobsMetricStats.filter(
|
||||
(data) => data.subCluster == subCluster.name,
|
||||
)
|
||||
)}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
{/each}
|
||||
{/if}
|
Reference in New Issue
Block a user